Синхронизация панелей TableLayoutPanels

У меня есть несколько TableLayoutPanels, каждый из которых отображает категорию информации имени/значения в двух столбцах — один с информационными метками, а другой с метками данных.

В каждом из них я установил для первого столбца автоматический размер и выровнял все метки по правому краю, что отлично работает. Однако он работает отдельно для каждой TableLayoutPanel (очевидно) и выглядит примерно так:

TableLayoutPanel 1:

+--------+--------+
| Name 1 | Data 1 |
+--------+--------+
| Name 2 | Data 2 |
+--------+--------+
| Name 3 | Data 3 |
+--------+--------+

TableLayoutPanel 2:

+------------------+--------+
|      Long Name 1 | Data 1 |
+------------------+--------+
| Very Long Name 2 | Data 2 |
+------------------+--------+
|      Long Name 3 | Data 3 |
+------------------+--------+

Я ищу способ учитывать все метки имен при автоматическом изменении размера всех первых столбцов, так что это выглядит так:

TableLayoutPanel 1:

+------------------+--------+
|           Name 1 | Data 1 |
+------------------+--------+
|           Name 2 | Data 2 |
+------------------+--------+
|           Name 3 | Data 3 |
+------------------+--------+

TableLayoutPanel 2:

+------------------+--------+
|      Long Name 1 | Data 1 |
+------------------+--------+
| Very Long Name 2 | Data 2 |
+------------------+--------+
|      Long Name 3 | Data 3 |
+------------------+--------+

Я не могу поместить все данные в одну таблицу, потому что каждая таблица представляет отдельную категорию информации и находится внутри пользовательского элемента управления с набором сворачиваемых панелей (поэтому вы можете показывать или скрывать каждую категорию отдельно).

Я пытался добиться этого, переопределив элементы управления контейнером OnLayout(), установив для всех первых столбцов TableLayoutPanels автоматический размер, получив всю их ширину, найдя максимум, а затем установив для всех своих первых столбцов фиксированный размер. наибольшей ширины. Это работает, но выглядит ужасно каждый раз, когда происходит макет, поскольку все столбцы переходят к автоматическому размеру, а затем возвращаются к фиксированному размеру.

Я предполагаю, что мне придется подключить ControlAdded и ControlRemoved для каждой таблицы, а затем SizeChanged для каждого дочернего элемента управления, чтобы узнать, когда размер любого дочернего элемента управления изменился, а затем вручную установить ширину столбца, но я м не уверен, как надежно получить правильную ширину.

Я попробовал вариант первого метода - используя GetPreferredSize() для всех элементов управления в первых столбцах, чтобы попытаться найти наибольшую ширину, а затем установить для всех первых столбцов фиксированный размер, но, похоже, возвращалась ширина, которая была немного к маленькому. Должен ли я применять дополнительный интервал?

Кто-нибудь знает какой-либо способ попросить TableLayoutPanel выполнить расчеты автоматического размера без их фактического применения визуально? Или, может быть, врать таблицам, чтобы «притвориться», что есть элемент управления определенной ширины, просто чтобы он его учитывал? Я не могу добавить фактические элементы управления, так как тогда для них потребуется выделить больше ячеек. Я попытался посмотреть исходный код с помощью ILSpy, но это некрасиво. Кажется, большую часть работы выполняет класс TableLayout, который, конечно же, является внутренним, и я не мог понять, что он делает.

Спасибо за любые идеи...


person Ashley    schedule 12.11.2011    source источник
comment
Я знаю, что вы можете определить, насколько длинным будет ваш текст, исходя из шрифта и текста. Можете ли вы сделать это для всего вашего текста, чтобы найти самый длинный, а затем вручную установить ширину первых столбцов для всех ваших панелей макета?   -  person Daryl    schedule 12.11.2011
comment
Кстати, хорошая работа с таблицами ascii...   -  person Daryl    schedule 12.11.2011
comment
Жаль, что вы не можете использовать WPF. Это тривиально в WPF.   -  person Ritch Melton    schedule 12.11.2011
comment
@Daryl: под я знаю, что ты можешь ты имеешь в виду, что ты знаешь как, или я знаю как? Ваша идея жизнеспособна, но, возможно, вам следует проиллюстрировать ее каким-нибудь кодом.   -  person Gert Arnold    schedule 13.11.2011
comment
@Daryl: я думаю, что в основном это то, что я получил, используя GetPreferredSize() для меток - казалось, что ширина была немного меньше для самого широкого элемента, поэтому я могу только предположить, что я' m пренебрегая несколькими пикселями заполнения или чего-то еще, хотя я не уверен, откуда. Я думаю, я могу просто +8 к ширине, которую я получаю, или что-то в этом роде, но было бы неплохо сделать это «правильно», если есть способ. Также я надеялся найти общее решение, в котором элементы управления не обязательно были бы метками... хотя, возможно, я слишком надеялся, что измерение текста решит мою насущную проблему.   -  person Ashley    schedule 13.11.2011


Ответы (3)


Вы можете использовать Graphics.Measurestring, чтобы определить длину в пикселях без фактического рисовать его. Есть некоторые небольшие недостатки, так что вы можете подумать о добавлении или удаление некоторых накладок. После теста или двух, вы можете быть довольно близко. Насколько я знаю, это самый правильный способ, и он не предполагает, что текст находится на этикетке.

Кроме того, попытка найти способ заставить TableLayoutPanel вычислять размеры, не отображая его визуально, звучит так, будто вы пытаетесь заставить его делать что-то, для чего он не предназначен.

person Daryl    schedule 13.11.2011
comment
Я остановился на GetPreferredSize(), так как он может работать с любым элементом управления, с текстом или без него. Спасибо за предложение. - person Ashley; 14.11.2011

Оказалось, что ширина, возвращаемая GetPreferredSize() была полезной, просто было «слишком поздно»; Я вычислял правильный размер и возвращал его в коде, вызываемом из метода OnLayout() TableLayoutPanels, и установка ширины столбца не имеет никакого эффекта до следующего макета.

У меня было решение, в котором использовался отдельный компонент, реализующий IExtenderProvider, который можно было использовать для объединения таблиц, но из-за описанной выше проблемы он всегда отставал от изменений управления. Даже перехватывая SizeChanged на всех дочерних элементах управления, TableLayoutPanel сначала получает событие и запускает компоновку.

Поэтому я не видел другого пути, кроме как переопределить сам процесс компоновки. Я попытался создать LayoutEngine, который выполнил необходимые вычисления, изменил размер столбцов, а затем делегировал фактическую работу над макетом старому механизму макета, но, к сожалению, Control.LayoutEngine доступен только для чтения, а TableLayoutPanel даже не использует резервное поле, он просто возвращает другой объект напрямую, поэтому я даже не мог обойти это, используя отражение для назначения частного резервного поля.

В конце концов мне пришлось прибегнуть к подклассу элемента управления, чтобы переопределить OnLayout(). Вот результат:

public class SynchronizedTableLayoutPanel : TableLayoutPanel
    {
    /// <summary>
    /// Specifies a key used to group <see cref="SynchronizedTableLayoutPanel"/>s together.
    /// </summary>
    public String SynchronizationKey
        {
        get { return _SynchronizationKey; }
        set
            {
            if (!String.IsNullOrEmpty(_SynchronizationKey))
                RemoveSyncTarget(this);

            _SynchronizationKey = value;

            if (!String.IsNullOrEmpty(value))
                AddSyncTarget(this);
            }
        } private String _SynchronizationKey;

    #region OnLayout(), Recalculate()

    protected override void OnLayout(LayoutEventArgs levent)
        {
        if (ColumnCount > 0 && !String.IsNullOrEmpty(SynchronizationKey))
            {
            Recalculate();
            ColumnStyles[0] = new ColumnStyle(SizeType.Absolute, GetMaxWidth(SynchronizationKey));
            }

        base.OnLayout(levent);
        }

    public void Recalculate()
        {
        var LargestWidth = Enumerable.Range(0, RowCount)
            .Select(i => GetControlFromPosition(0, i))
            .Where(c => c != null)
            .Select(c => (Int32?)((c.AutoSize ? c.GetPreferredSize(new Size(Width, 0)).Width : c.Width)+ c.Margin.Horizontal))
            .Max();

        SetMaxWidth(this, LargestWidth.GetValueOrDefault(0));
        }

    #endregion

    #region (Static) Data, cctor, AddSyncTarget(), RemoveSyncTarget(), SetMaxWidth(), GetMaxWidth()

    private static readonly Dictionary<SynchronizedTableLayoutPanel, Int32> Data;

    static SynchronizedTableLayoutPanel()
        {
        Data = new Dictionary<SynchronizedTableLayoutPanel, Int32>();
        }

    private static void AddSyncTarget(SynchronizedTableLayoutPanel table)
        {
        Data.Add(table, 0);
        }

    private static void RemoveSyncTarget(SynchronizedTableLayoutPanel table)
        {
        Data.Remove(table);
        }

    private static void SetMaxWidth(SynchronizedTableLayoutPanel table, Int32 width)
        {
        Data[table] = width;

        foreach (var pair in Data.ToArray())
            if (pair.Key.SynchronizationKey == table.SynchronizationKey && pair.Value != width)
                pair.Key.PerformLayout();
        }

    private static Int32 GetMaxWidth(String key)
        {
        var MaxWidth = Data
            .Where(p => p.Key.SynchronizationKey == key)
            .Max(p => (Int32?) p.Value);

        return MaxWidth.GetValueOrDefault(0);
        }

    #endregion
    }

Эта версия заботится только о первом столбце, но ее можно адаптировать для синхронизации других столбцов или строк.

person Ashley    schedule 13.11.2011

Этот подход не мерцает и не вызывает скачков при изменении размера:

public partial class Form1 : Form
{
    private readonly Timer _timer = new Timer();

    public Form1()
    {
        InitializeComponent();

        _timer.Interval = 500;
        _timer.Tick += (o, ea) => UpdateWithRandomSizes();
        _timer.Start();
    }

    private void UpdateWithRandomSizes()
    {
        var rand = new Random();
        label1.Text = new string('A', rand.Next(10));
        label2.Text = new string('B', rand.Next(10));
        label3.Text = new string('C', rand.Next(10));
        label4.Text = new string('D', rand.Next(10));

        tableLayoutPanel1.ColumnStyles[0].SizeType = SizeType.AutoSize;
        tableLayoutPanel2.ColumnStyles[0].SizeType = SizeType.AutoSize;
        var width1 = tableLayoutPanel1.GetColumnWidths()[0];
        var width2 = tableLayoutPanel2.GetColumnWidths()[0];

        var max = Math.Max(width1, width2);

        tableLayoutPanel1.ColumnStyles[0].Width = max;
        tableLayoutPanel1.ColumnStyles[0].SizeType = SizeType.Absolute;
        tableLayoutPanel2.ColumnStyles[0].Width = max;
        tableLayoutPanel2.ColumnStyles[0].SizeType = SizeType.Absolute;
    }
}
person Ritch Melton    schedule 12.11.2011
comment
Хм, по сути, это то, что я делал изначально, но это не сработало для меня. Я только что попробовал еще раз (скопировал и вставил ваш код), и действительно, с парой строк все в порядке, но с двумя таблицами всего по 15 строк в каждой весь процесс верстки занимает у меня почти секунду, и очень хорошо виден. «Прыжки» более очевидны, если вы даете один стол с длинными строками и один стол с короткими. Мой компьютер не совсем медленный (четырехъядерный Phenom II)... неужели TableLayoutPanel настолько неэффективен? - person Ashley; 14.11.2011
comment
SuspendLayout()/ResumeLayout() на таблицах перед изменением меток, кажется, устраняет поведение прыжков, но это все равно занимает почти секунду и заметно перерисовывается по крупицам. Возможно, двойная буферизация могла бы позаботиться о рисовании, но не о скорости. - person Ashley; 14.11.2011
comment
@ Эшли - Ааа, это отстой. Мне это было интересно, потому что мне было интересно, есть ли в WinForms эквивалент функции WPF, которая делает эту задачу тривиальной. - person Ritch Melton; 14.11.2011
comment
Я очень, очень хочу перейти на WPF. В моем текущем проекте я уже создал более 80 пользовательских элементов управления, и я думаю, что более 75% из них было бы ненужным или тривиальным для реализации в WPF. К сожалению, на данный момент я недостаточно знаком с архитектурой, чтобы рискнуть. - person Ashley; 14.11.2011
comment
Да, вы можете многое сделать с помощью winforms (flowlayout, привязка данных, разделение презентаторов), но для простого компостирования офигенных элементов управления WPF рулит. - person Ritch Melton; 14.11.2011