Пользовательский класс WPF Panel для автоматического определения размера
Я пытаюсь написать кастомPanel
класс для WPF, переопределяяMeasureOverride
а такжеArrangeOverride
но пока этов основном работая, я испытываю одну странную проблему, которую не могу объяснить.
В частности после звонкаArrange
на моих детских предметов вArrangeOverride
после выяснения того, какими должны быть их размеры, они не соответствуют размеру, который я им даю, и, кажется, измеряют размер, переданный ихMeasure
метод внутриMeasureOverride
.
Я что-то упустил в том, как эта система должна работать? Я понимаю, что призваниеMeasure
просто заставляет ребенка оценить егоDesiredSize
на основе предоставленного availableSize, и не должно влиять на его фактический конечный размер.
Вот мой полный код (кстати, Panel предназначена для того, чтобы расположить дочерние элементы максимально экономно, выделяя меньше места для строк, которые в этом не нуждаются, и равномерно распределяя оставшееся пространство среди остальных - в настоящее время она поддерживает только вертикальная ориентация, но я планирую добавить горизонтальную, как только у меня все получится):
Редактировать: Спасибо за ответы. Я рассмотрю их более подробно через минуту. Однако позвольте мне уточнить, как работает мой предполагаемый алгоритм, поскольку я этого не объяснял.
Прежде всего, лучший способ подумать о том, что я делаю, это представить сетку с каждой строкой, установленной на *. Это делит пространство равномерно. Однако в некоторых случаях элементу в строке может не понадобиться все это пространство; если это так, я хочу взять любой оставшийся пробел и дать его тем строкам, которые могли бы использовать пробел. Если никакие строки не нуждаются в дополнительном пространстве, я просто пытаюсь распределить вещи равномерно (вот чтоextraSpace
делает, это только для этого случая).
Я делаю это в два прохода. Конечной точкой первого прохода является определение окончательного «нормального размера» строки, т.е. размер строк, которые будут сокращены (с учетом размера, меньшего, чем его требуемый размер). Я делаю это, переходя от самого маленького предмета к самому большому и корректируя вычисленный нормальный размер на каждом шаге, добавляя оставшееся пространство от каждого маленького предмета к каждому последующему большему предмету до тех пор, пока больше не уместятся, а затем не сломаются предметы.
В следующем проходе я использую это нормальное значение, чтобы определить, подходит ли элемент или нет, просто взявMin
нормального размера с желаемым размером элемента.
(Я также изменил анонимный метод на лямбда-функцию для простоты.)
Изменить 2: Кажется, мой алгоритм отлично работает для определения правильного размера детей. Тем не менее, дети просто не принимают их данного размера. Я попробовал предложенный ГоблиномMeasureOverride
путем передачи PositiveInfinity и возврата Size (0,0), но это заставляет детей рисовать себя так, как будто нет никаких ограничений пространства. Часть, которая не является очевидной в том, что это происходит из-за вызоваMeasure
, Документация Microsoft по этому вопросу не совсем ясна, так как я несколько раз перечитывал описание каждого класса и свойства. Однако теперь ясно, чтоMeasure
на самом деле влияет на рендеринг потомка, поэтому я попытаюсь разделить логику между двумя функциями, как предложил BladeWise.
Решено !! Я получил это работает. Как я и подозревал, мне нужно было вызывать Measure () дважды для каждого ребенка (один раз, чтобы оценить DesiredSize, и второй, чтобы дать каждому ребенку его правильный рост). Мне кажется странным, что макет в WPF был бы спроектирован таким странным образом, когда он разделен на два прохода, но проход Измерения фактически делает две вещи: мерыа также Размеры детей и проход Arrange почти ничего не делают, кроме физического расположения детей. Очень странно.
Выложу рабочий код внизу.
Во-первых, оригинальный (неработающий) код:
protected override Size MeasureOverride( Size availableSize ) {
foreach ( UIElement child in Children )
child.Measure( availableSize );
return availableSize;
}
protected override System.Windows.Size ArrangeOverride( System.Windows.Size finalSize ) {
double extraSpace = 0.0;
var sortedChildren = Children.Cast<UIElement>().OrderBy<UIElement, double>( child=>child.DesiredSize.Height; );
double remainingSpace = finalSize.Height;
double normalSpace = 0.0;
int remainingChildren = Children.Count;
foreach ( UIElement child in sortedChildren ) {
normalSpace = remainingSpace / remainingChildren;
if ( child.DesiredSize.Height < normalSpace ) // if == there would be no point continuing as there would be no remaining space
remainingSpace -= child.DesiredSize.Height;
else {
remainingSpace = 0;
break;
}
remainingChildren--;
}
// this is only for cases where every child item fits (i.e. the above loop terminates normally):
extraSpace = remainingSpace / Children.Count;
double offset = 0.0;
foreach ( UIElement child in Children ) {
//child.Measure( new Size( finalSize.Width, normalSpace ) );
double value = Math.Min( child.DesiredSize.Height, normalSpace ) + extraSpace;
child.Arrange( new Rect( 0, offset, finalSize.Width, value ) );
offset += value;
}
return finalSize;
}
И вот рабочий код:
double _normalSpace = 0.0;
double _extraSpace = 0.0;
protected override Size MeasureOverride( Size availableSize ) {
// first pass to evaluate DesiredSize given available size:
foreach ( UIElement child in Children )
child.Measure( availableSize );
// now determine the "normal" size:
var sortedChildren = Children.Cast<UIElement>().OrderBy<UIElement, double>( child => child.DesiredSize.Height );
double remainingSpace = availableSize.Height;
int remainingChildren = Children.Count;
foreach ( UIElement child in sortedChildren ) {
_normalSpace = remainingSpace / remainingChildren;
if ( child.DesiredSize.Height < _normalSpace ) // if == there would be no point continuing as there would be no remaining space
remainingSpace -= child.DesiredSize.Height;
else {
remainingSpace = 0;
break;
}
remainingChildren--;
}
// there will be extra space if every child fits and the above loop terminates normally:
_extraSpace = remainingSpace / Children.Count; // divide the remaining space up evenly among all children
// second pass to give each child its proper available size:
foreach ( UIElement child in Children )
child.Measure( new Size( availableSize.Width, _normalSpace ) );
return availableSize;
}
protected override System.Windows.Size ArrangeOverride( System.Windows.Size finalSize ) {
double offset = 0.0;
foreach ( UIElement child in Children ) {
double value = Math.Min( child.DesiredSize.Height, _normalSpace ) + _extraSpace;
child.Arrange( new Rect( 0, offset, finalSize.Width, value ) );
offset += value;
}
return finalSize;
}
Это не может быть супер-эффективным, что с необходимостью звонитьMeasure
дважды (и итерацияChildren
три раза), но это работает. Любые оптимизации алгоритма приветствуются.