XAML WPF弹出窗口定位和绘制问题

de90aj5v  于 2022-12-07  发布在  其他
关注(0)|答案(1)|浏览(223)

我有一个UserControl,我在其中创建了一个样式,该样式稍后将应用于Popup的ContentControl(以下是UserControl中的所有定义):

<Style x:Key="ttPopupContent" TargetType="{x:Type ContentControl}">
        <Setter Property="Template">
            <Setter.Value>
                <!-- BOTTOM Popup -->
                <ControlTemplate TargetType="{x:Type ContentControl}">
                    <Grid x:Name="Grid">
                        <Grid.RowDefinitions>
                            <RowDefinition Height="20" />
                            <RowDefinition Height="*" />
                        </Grid.RowDefinitions>

                        <Rectangle MinWidth="40" Fill="#fff" Stroke="#FF000000" Grid.Row="1" />
                        <StackPanel Grid.Row="1">
                            <TextBlock Text="My popup title for bottom popup" TextWrapping="Wrap"/>
                            <TextBlock Text="My popup body content for bottom popup" TextWrapping="Wrap" />
                        </StackPanel>
                        <Path Fill="#fff" Stretch="Fill" Stroke="#FF000000" HorizontalAlignment="Left" 
                              Margin="{TemplateBinding Tag}" Width="20" Grid.Row="0" Data="M 0,21 L 10,0 20,21" />
                        <ContentPresenter Margin="8" Grid.Row="1" />
                    </Grid>
                </ControlTemplate>
            </Setter.Value>
        </Setter>
        <Style.Triggers>
            <DataTrigger Binding="{Binding Placement, ElementName=myPopup}" Value="Top">
                <!-- TOP Popup -->
                <Setter Property="Template">
                    <Setter.Value>
                        <ControlTemplate TargetType="{x:Type ContentControl}">
                            <Grid x:Name="Grid">
                                <Grid.RowDefinitions>
                                    <RowDefinition Height="*" />
                                    <RowDefinition Height="20" />
                                </Grid.RowDefinitions>

                                <Rectangle MinWidth="40" Fill="#fff" Stroke="#FF000000" Grid.Row="0" />
                                <StackPanel Grid.Row="0">
                                    <TextBlock Text="My popup title for top popup"
                               TextWrapping="Wrap"/>
                                    <TextBlock Text="My popup body content for top popup"
                               TextWrapping="Wrap" />
                                </StackPanel>
                                <Path Fill="#fff" Stretch="Fill" Stroke="#FF000000" HorizontalAlignment="Left" 
                              Margin="{TemplateBinding Tag}" Width="20" Grid.Row="1" Data="M 0,0 L 10,20 20,0" />
                                <ContentPresenter Margin="8" Grid.Row="0" />
                            </Grid>
                        </ControlTemplate>
                    </Setter.Value>
                </Setter>
            </DataTrigger>
        </Style.Triggers>
    </Style>

<!-- this label is placed at leftmost of the screen -->
<TextBlock x:Name="txtBlck1" Text="ShowInfo" />
<Popup x:Name="myPopup1" AllowsTransparency="True" Opened="Popup_Opened1"
         PlacementTarget="txtBlck1" Placement="Bottom">
    <ContentControl Style="{StaticResource ttPopupContent}"/>
</Popup>

<!-- this label is placed at rightmost of the screen -->
<TextBlock x:Name="txtBlck2" Text="AnotherLabel" />
<Popup x:Name="myPopup2" AllowsTransparency="True" Opened="Popup_Opened2"
         PlacementTarget="txtBlck2" Placement="Bottom">
    <ContentControl Style="{StaticResource ttPopupContent}"/>
</Popup>

有时它不能正确地将箭头指向弹出窗口绑定的标签。例如,如果我将鼠标悬停在标签“AnotherLabel”上,它将被绘制如下(该标签位于屏幕的最右边):

正如你所看到的,箭头没有放在正确的位置。2然而,我有另一个标签“ShowInfo”放在屏幕的最左边,然后它就工作了:

因此,我尝试通过在代码隐藏(xaml.cs)中执行以下操作来调整箭头的水平对齐,以正确指向标签:

private void Popup_Opened1(object sender, EventArgs e)
    {
        UIElement target = myPopup1.PlacementTarget;
        Point adjust = target.TranslatePoint(new Point(8, 0), popup);
        if (adjust.Y > 0)
        {
            popup.Placement = System.Windows.Controls.Primitives.PlacementMode.Top;
        }
        myPopup1.Tag = new Thickness(adjust.X, -1.5, 0, -1.5);
    }

    private void Popup_Opened2(object sender, EventArgs e)
    {
        UIElement target = myPopup2.PlacementTarget;
        Point adjust = target.TranslatePoint(new Point(8, 0), popup);
        if (adjust.Y > 0)
        {
            popup.Placement = System.Windows.Controls.Primitives.PlacementMode.Top;
        }
        myPopup2.Tag = new Thickness(adjust.X, -1.5, 0, -1.5);
    }

我在代码隐藏中尝试做的是通过水平调整箭头,将其放置在正确的位置,如here所述(在这种情况下是一个工具提示,在我的情况下是一个弹出窗口)。
我有两个问题:
1.水平调整箭头以正确指向标签。
1.箭头未正确绘制,其下方显示一条黑线。请参见下图:

mfpqipee

mfpqipee1#

您应该扩展Popup以方便地实现所需的行为。我建议在内部将Popup.Placement设置为PlacementMode.Custom以启用自定义定位。
这使您可以轻松访问定位上下文,以便检测放置目标何时位于屏幕右侧(换句话说,Popup与父Window右对齐)。
箭头框和工具提示内容应由自定义控件承载。
若要正确绘制箭头方块,您应该建立并绘制几何图形,例如覆写自订控件的UIElement.OnRender
为了简单起见,下面的示例只支持右下角的位置。您可以按照简单的模式扩展功能以支持其他定位(阅读代码中的注解):

public class MyToolTip : Popup
{
  internal enum Side
  {
    None = 0,
    Left,
    Top,
    Right,
    Bottom
  }

  private MyToolTipContent ToolTipContent { get; }

  static MyToolTip()
  {
    PlacementTargetProperty.OverrideMetadata(typeof(MyToolTip), new FrameworkPropertyMetadata(default(object), OnPlacementTargetChanged));
    PlacementProperty.OverrideMetadata(typeof(MyToolTip), new FrameworkPropertyMetadata(null, CoercePlacement));
  }

  public MyToolTip()
  {
    this.ToolTipContent = new MyToolTipContent();
    this.Child = this.ToolTipContent;

    // Enable custom placement to get context related placement info
    // like size and positions.
    this.CustomPopupPlacementCallback = CalculatePosition;
    this.Placement = PlacementMode.Custom;

    this.AllowsTransparency = true;
  }

  private static void OnPlacementTargetChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
    => (d as MyToolTip).OnPlacementTargetChanged(e.OldValue as UIElement, e.NewValue as UIElement);

  private static object CoercePlacement(DependencyObject d, object baseValue)
  {
    // Backup/delegate original PlacementMode as we need to override it
    // in order to enable custom placement (and to keep it enabled by force).
    (d as MyToolTip).ToolTipContent.Placement = (PlacementMode)baseValue;

    // Enforce custom placement (invokation of the Popup.CustomPopupPlacementCallback)
    return PlacementMode.Custom;
  }

  // Show Popup on mouse hover over the placement target
  protected virtual void OnPlacementTargetChanged(UIElement oldTarget, UIElement newTarget)
  {
    if (oldTarget is not null)
    {
      newTarget.MouseEnter -= OnTargetMouseEnter;
      newTarget.MouseLeave -= OnTargetMouseLeave;
    }

    if (newTarget is not null)
    {
      newTarget.MouseEnter += OnTargetMouseEnter;
      newTarget.MouseLeave += OnTargetMouseLeave;
    }
  }

  private void OnTargetMouseLeave(object sender, MouseEventArgs e) => this.IsOpen = false;
  private void OnTargetMouseEnter(object sender, MouseEventArgs e) => this.IsOpen = true;

  private CustomPopupPlacement[] CalculatePosition(Size popupSize, Size targetSize, Point offset)
  {
    Side clippingSide = GetParentWindowClippingSide(targetSize);

    // TODO::Handle clipping of remaining sides (top and bottom. Left side will always fit).
    switch (clippingSide)
    {
      // Popup will clip right Window bounds => shift Popup to the left to compensate
      // (i.e. right align with parent Window).
      case Side.Right:
        AlignRightSide(popupSize, ref offset, targetSize);
        break;
    }

    // TODO::Handle remaining modes
    switch (this.ToolTipContent.Placement)
    {
      case PlacementMode.Bottom:
        offset.Offset(0, targetSize.Height);
        break;
      case PlacementMode.Top:
        break;
    }

    // Enforce OnRender to update the visual with the latest instructions
    this.ToolTipContent.InvalidateVisual();

    return new[] { new CustomPopupPlacement(offset, PopupPrimaryAxis.None) };
  }

  private void AlignRightSide(Size popupSize, ref Point offset, Size targetSize)
  {
    offset = new Point(targetSize.Width, 0);
    offset.Offset(-popupSize.Width, 0);
    this.ToolTipContent.HorizontalToolTipArrowAlignment = HorizontalAlignment.Right;
  }

  private Side GetParentWindowClippingSide(Size targetSize)
  {
    Window parentWindow = Window.GetWindow(this.PlacementTarget);
    var parentWindowBounds = new Rect(new Point(), parentWindow.RenderSize);
    Point targetPositionInParentWindow = this.PlacementTarget.TranslatePoint(new Point(), parentWindow);
    var targetBounds = new Rect(targetPositionInParentWindow, targetSize);

    /* Check for clipping */
    bool isTargetRightSideInParentWindowBounds = parentWindowBounds.Contains(targetBounds.TopRight);
    if (!isTargetRightSideInParentWindowBounds)
    {
      return Side.Right;
    }

    // TODO::Check if Popup clips top or  bottom Window bounds following the above pattern. If it does, switch to Bottom/Top placement
    // to make it fit the parent's bounds by changing the OriginalPlacementMode accordingly.
    // If you don't want to overwrite the original value introduce a dedicated property
    // that you really depend on (for example OriginalPlacementModeInternal).

    // Popup completely fits the parent using the desired placement.
    return Side.None;
  }
}

我的工具提示内容.cs

该自定义控件组成Popup的实际内容,负责绘制可视的几何图形。

internal class MyToolTipContent : Control
{
  public PlacementMode Placement
  {
    get => (PlacementMode)GetValue(PlacementProperty); 
    set => SetValue(PlacementProperty, value); 
  }

  public static readonly DependencyProperty PlacementProperty = DependencyProperty.Register(
    "Placement",
    typeof(PlacementMode),
    typeof(MyToolTipContent),
    new PropertyMetadata(default));

  private const double ArrowHeight = 12;
  private const double ArrowWidth = 12;

  internal HorizontalAlignment HorizontalToolTipArrowAlignment { get; set; }

  static MyToolTipContent()
  {
    DefaultStyleKeyProperty.OverrideMetadata(typeof(MyToolTipContent), new FrameworkPropertyMetadata(typeof(MyToolTipContent)));
  }

  protected override void OnRender(DrawingContext drawingContext)
  {
    SolidColorBrush fillBrush = Brushes.Transparent;
    var strokePen = new Pen(Brushes.Black, 2);

    // Make room for the stroke
    this.Margin = new Thickness(strokePen.Thickness);

    var toolTipGeometry = new PathGeometry();

   /* Create geometry depending on tool tip position.
    *
    * TODO::Draw different geometries based on different placement.
    *
    */
    if (this.HorizontalToolTipArrowAlignment == HorizontalAlignment.Right
      && this.Placement == PlacementMode.Bottom)
    {
      // "Push" content down to make room for the arrow
      this.Padding = new Thickness(0, ArrowHeight, 0, 0);

      var arrowBoxLines = new PolyLineSegment(new[]
      {
        new Point(this.RenderSize.Width - ArrowWidth, ArrowHeight),
        new Point(this.RenderSize.Width - ArrowWidth / 2, 0),
        new Point(this.RenderSize.Width, ArrowHeight),
        new Point(this.RenderSize.Width, this.RenderSize.Height),
        new Point(0, this.RenderSize.Height),
      }, true);

      var arrowBoxPath = new PathFigure(
        new Point(0, ArrowHeight), 
        new[] { arrowBoxLines }, 
        true);

      toolTipGeometry.Figures.Add(arrowBoxPath);
    }

    drawingContext.DrawGeometry(fillBrush, strokePen, toolTipGeometry);
    base.OnRender(drawingContext);
  }
}

一般.xaml

<Style TargetType="{x:Type local:MyToolTipContent}">
  <Setter Property="Template">
    <Setter.Value>
      <ControlTemplate TargetType="Control">
        <Border Padding="{TemplateBinding Padding}">
          <StackPanel>
            <TextBlock Text="My popup title for bottom popup"
                        TextWrapping="Wrap" />
            <TextBlock Text="My popup body content for bottom popup"
                        TextWrapping="Wrap" />
          </StackPanel>
        </Border>
      </ControlTemplate>
    </Setter.Value>
  </Setter>
</Style>

相关问题