Я разрабатываю собственный компонент WPF Flyout, который использует Popup с CustomPopupPlacementCallback. Всплывающее меню позиционируется правильно, если окно имеет FlowDirection="LeftToRight", но когда я устанавливаю FlowDirection="RightToLeft" в окне, всплывающее окно отображается в неправильном положении - часто полностью не совпадает с целью размещения.
Среда
- Framework: .NET 9.0 / WPF
- Проблема: Неправильное размещение пользовательского всплывающего окна в макетах RTL
- Затронутые компоненты: CustomPopupPlacementCallback, PopupPositioner
Моя реализация состоит из трех основных компонентов:
- CustomPopupPlacementHelper – вычисляет позиции всплывающих окон с помощью CustomPopupPlacementCallback.
- PopupPositioner – использует внутренние методы WPF для расширенного позиционирования.
- FlyoutBase – основной элемент управления всплывающим окном, управляющий всплывающим окном.
Попытка 1: ручное зеркалирование RTL в CustomPopupPlacementHelper
Я обнаружил RTL FlowDirection и вручную поменял местами координаты:
Код: Выделить всё
private static bool ShouldMirrorForRTL(FrameworkElement child)
{
var popup = FindParentPopup(child);
if (popup?.PlacementTarget is FrameworkElement target)
{
return target.FlowDirection == FlowDirection.RightToLeft;
}
return false;
}
// In CalculatePopupPlacement:
case CustomPlacementMode.TopEdgeAlignedLeft:
point = shouldMirrorForRTL
? new Point(targetSize.Width - popupSize.Width, -popupSize.Height)
: new Point(0, -popupSize.Height);
break;
Попытка 2: коррекция RTL в PopupPositioner
Я добавил коррекцию RTL после расчета позиции:
Код: Выделить всё
if (_popup.PlacementTarget is FrameworkElement feRtl &&
feRtl.FlowDirection == FlowDirection.RightToLeft)
{
if (PlacementInternal == PlacementMode.Left ||
PlacementInternal == PlacementMode.Right)
{
double mirroredX = bestTranslation.X + (targetWidth - popupWidth)
- 2 * (bestTranslation.X - targetBounds.Left);
bestTranslation.X = mirroredX;
}
}
Попытка 3: скорректированный PlacementRectangle во FlyoutBase
Я попробовал настроить прямоугольник размещения для RTL:
Код: Выделить всё
bool rtl = target is FrameworkElement fe &&
fe.FlowDirection == FlowDirection.RightToLeft;
if (rtl)
{
// Move rect left by target width so right edge (x=0) aligns with target's right edge
value = new Rect(
new Point(-targetSize.Width, -Offset),
new Point(0, targetSize.Height + Offset));
}
Анализ первопричин
После исследования исходного кода WPF Popup (из справочного источника) я обнаружил, что WPF Popup имеет встроенную поддержку RTL:
- Отмена преобразования — всплывающее окно автоматически применяет преобразование масштаба для отмены зеркального отображения FlowDirection:
Код: Выделить всё
if (parent != null && (FlowDirection)parent.GetValue(FlowDirectionProperty) == FlowDirection.RightToLeft) { popupTransform.Scale(-1.0, 1.0); // Undo FlowDirection Mirror } - Обмен точками интереса. Когда FlowDirection у целевого и дочернего элемента различается, WPF меняет точки интереса:
Код: Выделить всё
if ((FlowDirection)target.GetValue(FlowDirectionProperty) != (FlowDirection)child.GetValue(FlowDirectionProperty)) { SwapPoints(ref interestPoints[(int)InterestPoint.TopLeft], ref interestPoints[(int)InterestPoint.TopRight]); SwapPoints(ref interestPoints[(int)InterestPoint.BottomLeft], ref interestPoints[(int)InterestPoint.BottomRight]); }
Удалите все ручные преобразования RTL и позвольте WPF Popup обрабатывать RTL собственными средствами.
Внесенные изменения:
- CustomPopupPlacementHelper – удалено FollowMirrorForRTL() и вся логика позиционирования, специфичная для RTL.
- PopupPositioner – удалены ручные исправления RTL.
- FlyoutBase – упрощен GetPlacementRectangle(), чтобы всегда использовать стандартные координаты.
Код: Выделить всё
internal static CustomPopupPlacement[] PositionPopup(
CustomPlacementMode placement,
Size popupSize,
Size targetSize,
Point offset,
FrameworkElement child = null)
{
Matrix transformToDevice = default;
if (child != null)
{
Helper.TryGetTransformToDevice(child, out transformToDevice);
}
// Let WPF Popup handle RTL natively - no manual RTL transformations
CustomPopupPlacement preferredPlacement = CalculatePopupPlacement(
placement, popupSize, targetSize, offset, child, transformToDevice);
// ... rest of the method
}
private static CustomPopupPlacement CalculatePopupPlacement(
CustomPlacementMode placement,
Size popupSize,
Size targetSize,
Point offset,
FrameworkElement child = null,
Matrix transformToDevice = default)
{
Point point;
PopupPrimaryAxis primaryAxis;
switch (placement)
{
case CustomPlacementMode.TopEdgeAlignedLeft:
// Always use standard LTR coordinates
point = new Point(0, -popupSize.Height);
primaryAxis = PopupPrimaryAxis.Horizontal;
break;
case CustomPlacementMode.TopEdgeAlignedRight:
point = new Point(targetSize.Width - popupSize.Width, -popupSize.Height);
primaryAxis = PopupPrimaryAxis.Horizontal;
break;
// ... other cases
}
return new CustomPopupPlacement(point, primaryAxis);
}
Код: Выделить всё
internal Rect GetPlacementRectangle(UIElement target)
{
Rect value = Rect.Empty;
if (target != null)
{
Size targetSize = target.RenderSize;
// Simple placement rectangle without RTL transformations
// WPF Popup will handle RTL layout natively
switch (Placement)
{
case FlyoutPlacementMode.Top:
case FlyoutPlacementMode.Bottom:
case FlyoutPlacementMode.TopEdgeAlignedLeft:
case FlyoutPlacementMode.TopEdgeAlignedRight:
case FlyoutPlacementMode.BottomEdgeAlignedLeft:
case FlyoutPlacementMode.BottomEdgeAlignedRight:
value = new Rect(
new Point(0, -Offset),
new Point(targetSize.Width, targetSize.Height + Offset));
break;
case FlyoutPlacementMode.Left:
case FlyoutPlacementMode.Right:
// ... other horizontal placements
value = new Rect(
new Point(-Offset, 0),
new Point(targetSize.Width + Offset, targetSize.Height));
break;
}
}
return value;
}
- Как мне обрабатывать RTL в пользовательской логике размещения всплывающих окон? Должен ли я:
- Разрешить WPF полностью обрабатывать RTL и избегать любых ручных преобразований?
- Обнаруживать RTL и отражать мои расчеты?
- Сравнить FlowDirection целевого объекта и дочерний элемент, как это делает WPF?
- Должны ли вычисления CustomPopupPlacementCallback поддерживать RTL? Или WPF применяет преобразования RTL после возврата обратного вызова?
- Как правильно определить, когда следует применять зеркальное отображение RTL? Это:
- Когда окно FlowDirection = RightToLeft?
- Когда целевой и всплывающий дочерние элементы имеют разные значения FlowDirection?
- Что-то еще?
Когда FlowDirection="RightToLeft":
- должен быть выровнен по визуальному левому краю (справа в координатах RTL)
Код: Выделить всё
BottomEdgeAlignedLeft - должен выравниваться по визуальному правому краю (слева в координатах RTL)
Код: Выделить всё
BottomEdgeAlignedRight - Всплывающие меню с выравниванием по центру должны оставаться по центру
- Все места размещения должны соблюдать порядок макета RTL
- Компонент отлично работает с FlowDirection="LeftToRight"
- Я пытаюсь обеспечить совместимость с собственным поведением RTL WPF.
- Компонент поддерживает анимацию, регулировку радиуса угла и инверсию смещения.
- Полный исходный код: WPF-Flyout на GitHub.
- Поведение WPF при размещении всплывающих окон
- WPF FlowDirection
- CustomPopupPlacementCallback
Подробнее здесь: https://stackoverflow.com/questions/798 ... ighttoleft
Мобильная версия