wpf - 将弹出窗口位置锁定到元素,或伪造带有图层的弹出窗口以在 ItemsControl 中进行就地编辑

标签 wpf popup z-index edit-in-place

我想要实现的本质上是对 wpf 中 ItemsControl 内的数据绑定(bind)对象进行就地编辑。

我的 ItemsControl 是一个水平的 WrapPanel,它包含一个用户控件 (NameControl) 的多个实例,它显示为带有人名的粉红色 Glyph .看起来像这样

Fig1

通过弹出窗口,我可以为这个“名称”显示一个编辑器(绑定(bind)对象的其他属性,如 AddressGender 等)并且这有效绝对没问题。此时我的 XAML 将遵循

<Style x:Key="NamesStyle" TargetType="{x:Type ItemsControl}">
    <Setter Property="ItemsPanel">
        <Setter.Value>
            <ItemsPanelTemplate>
                <WrapPanel Orientation="Horizontal" />
            </ItemsPanelTemplate>
        </Setter.Value>
    </Setter>
    <Setter Property="ItemTemplate">
        <Setter.Value>
            <DataTemplate>
            <StackPanel>
                <Button Command="{Binding EditName}" BorderThickness="0" Background="Transparent" Panel.ZIndex="1">
                    <widgets:NameControl />
                </Button>
                <Popup IsOpen="{Binding IsEditMode}"
                            PlacementTarget="{Binding ElementName=button}"
                            Margin="0 5 0 0" Placement="Relative" AllowsTransparency="True" >

                <Border Background="White" BorderBrush="DarkOrchid" BorderThickness="1,1,1,1" CornerRadius="5,5,5,5" 
                        Panel.ZIndex="100">
                    <Grid ShowGridLines="False" Margin="5" Background="White" Width="300">
                        <!-- Grid Content - just editor fields/button etc -->
                    </Grid>
                </Border>
                </Popup>
            </StackPanel>
        </DataTemplate>
        </Setter.Value>
    </Setter>
</Style>

当我点击一个看起来像这样的名字时给出一个输出

Fig2

我很高兴看到这个样子(除了我糟糕的颜色选择!!)除了弹出窗口不随寡妇移动(调整大小/最小化/最大化)并且弹出窗口在一切之上甚至其他窗口。

因此,解决部分问题的一种方法是将弹出窗口位置“附加”或锁定到元素。我还没有找到一个好的/简单的/xaml 方法来做到这一点。我遇到过一些基于代码的解决方案,但我不确定自己是否喜欢。它只是有点气味。

我试图实现的另一个解决方案是放弃弹出窗口并尝试模拟位于其他名称上方但位于(或下方,我不挑剔)相关名称控件上方的层/面板的行为。

我尝试了一些不同的东西,主要是关于在 PanelControl 中的控件上设置 Panel.ZIndex(Grid、WrapPanel、我的最顶部的 DockPanel主窗口)收效甚微。我已经实现了一个简单的 BoolToVisibilityConverter 来将我的编辑器 Grid 的 Visibility 属性绑定(bind)到我的 IsEditMode View 模型属性并且工作正常,但我不能我的生活在 ItemsControl 中安排我的元素以在名称上显示编辑器网格。

要执行上述操作,我只是注释掉 Popup 并将以下绑定(bind)添加到包含编辑器网格 Visibility="{Binding> 的 Border IsEditMode, Converter={StaticResource boolToVisibility}}"

所做的就是这样:

Fig3

它只是在名称​​ 下方显示弹出窗口,而不是 其他名称上方。

有什么帮助吗?我究竟做错了什么?

最佳答案

对我来说,这听起来像是 AdornerLayer 的工作。

我的实现一次只会显示一个“弹出窗口”,您可以通过再次单击该按钮来隐藏它。但是您也可以向 ContactAdorner 添加一个小的关闭按钮,或者坚持使用您的 OK 按钮,或者使用 IsHitTestVisible 元素填充 ContactAdorner 后面的 AdornerLayer 并通过隐藏打开的 Adorner 对点击使用react(因此点击外面的任何地方都会关闭弹出窗口) .

编辑:根据您的要求添加了小关闭按钮。 ContactAdorner 和 ContactDetailsTemplate 的变化。

您可能想要添加的另一件事是装饰器从底部剪裁后重新定位(我只检查从右侧剪裁)。

enter image description here

Xaml:

<UserControl x:Class="WpfApplication1.ItemsControlAdorner"
                 xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
                 xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
                 xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" 
                 xmlns:d="http://schemas.microsoft.com/expression/blend/2008" 
                 xmlns:i="http://schemas.microsoft.com/expression/2010/interactivity"
                 mc:Ignorable="d" 
                 xmlns:local="clr-namespace:WpfApplication1"
                 d:DesignHeight="300" d:DesignWidth="300">

    <UserControl.DataContext>
        <local:ViewModel />
    </UserControl.DataContext>

    <UserControl.Resources>
        <local:EnumToBooleanConverter x:Key="EnumToBooleanConverter" />

        <!-- Template for the Adorner -->
        <DataTemplate x:Key="ContactDetailsTemplate" DataType="{x:Type local:MyContact}" >
            <Border Background="#BBFFFFFF" BorderBrush="DarkOrchid" BorderThickness="1" CornerRadius="5" TextElement.Foreground="DarkOrchid" >
                <Grid Margin="5" Width="300">
                    <Grid.RowDefinitions>
                        <RowDefinition Height="Auto" />
                        <RowDefinition Height="Auto" />
                        <RowDefinition Height="Auto" />
                        <RowDefinition Height="Auto" />
                    </Grid.RowDefinitions>
                    <Grid.ColumnDefinitions>
                        <ColumnDefinition Width="*"/>
                        <ColumnDefinition Width="*"/>
                        <ColumnDefinition Width="Auto"/>
                    </Grid.ColumnDefinitions>
                    <TextBlock Text="Full name" />
                    <TextBox Grid.Row="1" Text="{Binding FullName, UpdateSourceTrigger=PropertyChanged}" />
                    <TextBlock  Grid.Row="2" Text="Address" />
                    <TextBox Grid.Row="3" Grid.ColumnSpan="2" Text="{Binding Address}" />
                    <TextBlock Grid.Column="1" Text="Gender" />
                    <StackPanel Orientation="Horizontal" Grid.Column="1" Grid.Row="1" >
                        <RadioButton Content="Male" IsChecked="{Binding Gender, Converter={StaticResource EnumToBooleanConverter}, ConverterParameter={x:Static local:Gender.Male}}" />
                        <RadioButton Content="Female" IsChecked="{Binding Gender, Converter={StaticResource EnumToBooleanConverter}, ConverterParameter={x:Static local:Gender.Female}}" />
                    </StackPanel>
                    <Button x:Name="PART_CloseButton" Grid.Column="2" Height="16">
                        <Button.Template>
                            <ControlTemplate>
                                <Border Background="#01FFFFFF" Padding="3" >
                                    <Path Stretch="Uniform" ClipToBounds="True" Stroke="DarkOrchid" StrokeThickness="2.5" Data="M 85.364473,6.9977109 6.0640998,86.29808 6.5333398,85.76586 M 6.9926698,7.4977169 86.293043,86.79809 85.760823,86.32885"  />
                                </Border>
                            </ControlTemplate>
                        </Button.Template>
                    </Button>
                </Grid>
            </Border>
        </DataTemplate>

        <!-- Button/Item style -->
        <Style x:Key="ButtonStyle1" TargetType="{x:Type Button}" >
            <Setter Property="Foreground" Value="White" />
            <Setter Property="FontFamily" Value="Times New Roman" />
            <Setter Property="Background" Value="#CC99E6" />
            <Setter Property="BorderThickness" Value="0" />
            <Setter Property="MinHeight" Value="24" />
            <Setter Property="Margin" Value="3,2" />
            <Setter Property="Padding" Value="3,2" />
            <Setter Property="Border.CornerRadius" Value="8" />
            <Setter Property="Template">
                <Setter.Value>
                    <ControlTemplate TargetType="Button">
                        <Border CornerRadius="{TemplateBinding Border.CornerRadius}" BorderBrush="{TemplateBinding BorderBrush}" BorderThickness="{TemplateBinding BorderThickness}" Background="{TemplateBinding Background}" Padding="{TemplateBinding Padding}" Margin="{TemplateBinding Margin}" >
                            <ContentPresenter VerticalAlignment="Center" HorizontalAlignment="Center" />
                        </Border>
                    </ControlTemplate>
                </Setter.Value>
            </Setter>
        </Style>

        <!-- ItemsControl style -->
        <Style x:Key="NamesStyle" TargetType="{x:Type ItemsControl}">
            <Setter Property="ItemsPanel">
                <Setter.Value>
                    <ItemsPanelTemplate>
                        <WrapPanel Orientation="Horizontal" />
                    </ItemsPanelTemplate>
                </Setter.Value>
            </Setter>
            <Setter Property="ItemTemplate">
                <Setter.Value>
                    <DataTemplate>
                        <Button x:Name="button" Style="{StaticResource ButtonStyle1}" Content="{Binding FullName}" >
                            <i:Interaction.Behaviors>
                                <local:ShowAdornerBehavior DataTemplate="{StaticResource ContactDetailsTemplate}" />
                            </i:Interaction.Behaviors>
                        </Button>
                    </DataTemplate>
                </Setter.Value>
            </Setter>
        </Style>
    </UserControl.Resources>

    <Grid>
        <ItemsControl ItemsSource="{Binding MyContacts}" Style="{StaticResource NamesStyle}" />
    </Grid>

</UserControl>

ShowAdornerBehavior、ContactAdorner、EnumToBooleanConverter:

using System.Windows;
using System.Linq;
using System.Windows.Controls;
using System.Windows.Documents;
using System.Windows.Interactivity;
using System.Windows.Media;
using System.Windows.Data;
using System;
namespace WpfApplication1
{
    public class ShowAdornerBehavior : Behavior<Button>
    {
        public DataTemplate DataTemplate { get; set; }

        protected override void OnAttached()
        {
            this.AssociatedObject.Click += AssociatedObject_Click;
            base.OnAttached();
        }

        void AssociatedObject_Click(object sender, RoutedEventArgs e)
        {
            var adornerLayer = AdornerLayer.GetAdornerLayer(this.AssociatedObject);
            var contactAdorner = new ContactAdorner(this.AssociatedObject, adornerLayer, this.AssociatedObject.DataContext, this.DataTemplate);
        }
    }

    public class ContactAdorner : Adorner
    {
        private ContentPresenter _contentPresenter;
        private AdornerLayer _adornerLayer;
        private static Button _btn;
        private VisualCollection _visualChildren;

        private double _marginRight = 5;
        private double _adornerDistance = 5;
        private PointCollection _points;

        private static ContactAdorner _currentInstance;

        public ContactAdorner(Button adornedElement, AdornerLayer adornerLayer, object data, DataTemplate dataTemplate)
            : base(adornedElement)
        {
            if (_currentInstance != null)
                _currentInstance.Hide(); // hides other adorners of the same type

            if (_btn != null && _btn == adornedElement)
            {
                _currentInstance.Hide(); // hides the adorner of this button (toggle)
                _btn = null;
            }
            else
            {
                _adornerLayer = adornerLayer;
                _btn = adornedElement;

                // adjust position if sizes change
                _adornerLayer.SizeChanged += (s, e) => { UpdatePosition(); };
                _btn.SizeChanged += (s, e) => { UpdatePosition(); };

                _contentPresenter = new ContentPresenter() { Content = data, ContentTemplate = dataTemplate };

                // apply template explicitly: http://stackoverflow.com/questions/5679648/why-would-this-contenttemplate-findname-throw-an-invalidoperationexception-on
                _contentPresenter.ApplyTemplate();

                // get close button from datatemplate
                Button closeBtn = _contentPresenter.ContentTemplate.FindName("PART_CloseButton", _contentPresenter) as Button;
                if (closeBtn != null)
                    closeBtn.Click += (s, e) => { this.Hide(); _btn = null; };

                _visualChildren = new VisualCollection(this); // this is needed for user interaction with the adorner layer
                _visualChildren.Add(_contentPresenter);

                _adornerLayer.Add(this);

                _currentInstance = this;

                UpdatePosition(); // position adorner
            }
        }


        /// <summary>
        /// Positioning is a bit fiddly. 
        /// Also, this method is only dealing with the right clip, not yet with the bottom clip.
        /// </summary>
        private void UpdatePosition()
        {
            double marginLeft = 0;
            _contentPresenter.Margin = new Thickness(marginLeft, 0, _marginRight, 0); // "reset" margin to get a good measure pass
            _contentPresenter.Measure(_adornerLayer.RenderSize); // measure the contentpresenter to get a DesiredSize
            var contentRect = new Rect(_contentPresenter.DesiredSize);
            double right = _btn.TranslatePoint(new Point(contentRect.Width, 0), _adornerLayer).X; // this does not work with the contentpresenter, so use _adornedElement

            if (right > _adornerLayer.ActualWidth) // if adorner is clipped by right window border, move it to the left
                marginLeft = _adornerLayer.ActualWidth - right;

            _contentPresenter.Margin = new Thickness(marginLeft, _btn.ActualHeight + _adornerDistance, _marginRight, 0); // position adorner

            DrawArrow();
        }

        private void DrawArrow()
        {
            Point bottomMiddleButton = new Point(_btn.ActualWidth / 2, _btn.ActualHeight - _btn.Margin.Bottom);
            Point topLeftAdorner = new Point(_btn.ActualWidth / 2 - 10, _contentPresenter.Margin.Top);
            Point topRightAdorner = new Point(_btn.ActualWidth / 2 + 10, _contentPresenter.Margin.Top);

            PointCollection points = new PointCollection();
            points.Add(bottomMiddleButton);
            points.Add(topLeftAdorner);
            points.Add(topRightAdorner);

            _points = points; // actual drawing executed in OnRender
        }

        protected override void OnRender(DrawingContext drawingContext)
        {
            // Drawing the arrow
            StreamGeometry streamGeometry = new StreamGeometry();
            using (StreamGeometryContext geometryContext = streamGeometry.Open())
            {
                if (_points != null && _points.Any())
                {
                    geometryContext.BeginFigure(_points[0], true, true);
                    geometryContext.PolyLineTo(_points.Where(p => _points.IndexOf(p) > 0).ToList(), true, true);
                }
            }

            // Draw the polygon visual
            drawingContext.DrawGeometry(Brushes.DarkOrchid, new Pen(_btn.Background, 0.5), streamGeometry);

            base.OnRender(drawingContext);
        }

        private void Hide()
        {
            _adornerLayer.Remove(this);
        }

        protected override Size MeasureOverride(Size constraint)
        {
            _contentPresenter.Measure(constraint);
            return _contentPresenter.DesiredSize;
        }

        protected override Size ArrangeOverride(Size finalSize)
        {
            _contentPresenter.Arrange(new Rect(finalSize));
            return finalSize;
        }

        protected override Visual GetVisualChild(int index)
        {
            return _visualChildren[index];
        }

        protected override int VisualChildrenCount
        {
            get { return _visualChildren.Count; }
        }
    }

    // http://stackoverflow.com/questions/397556/how-to-bind-radiobuttons-to-an-enum
    public class EnumToBooleanConverter : IValueConverter
    {
        public object Convert(object value, Type targetType, object parameter, System.Globalization.CultureInfo culture)
        {
            return value.Equals(parameter);
        }

        public object ConvertBack(object value, Type targetType, object parameter, System.Globalization.CultureInfo culture)
        {
            return value.Equals(true) ? parameter : Binding.DoNothing;
        }
    }
}

View 模型、我的联系人:

using System;
using System.Collections.ObjectModel;
using System.ComponentModel;
using System.Windows.Input;
namespace WpfApplication1
{
    public class ViewModel : INotifyPropertyChanged
    {
        public event PropertyChangedEventHandler PropertyChanged;
        private void OnPropertyChanged(string propertyName)
        {
            if (this.PropertyChanged != null)
                PropertyChanged(this, new PropertyChangedEventArgs(propertyName));
        }

        private ObservableCollection<MyContact> _myContacts = new ObservableCollection<MyContact>();
        public ObservableCollection<MyContact> MyContacts { get { return _myContacts; } set { _myContacts = value; OnPropertyChanged("MyContacts"); } }


        public ViewModel()
        {
            MyContacts = new ObservableCollection<MyContact>()
            {
                new MyContact() { FullName = "Sigmund Freud", Gender = Gender.Male },
                new MyContact() { FullName = "Abraham Lincoln", Gender = Gender.Male },
                new MyContact() { FullName = "Joan Of Arc", Gender = Gender.Female },
                new MyContact() { FullName = "Bob the Khann", Gender = Gender.Male, Address = "Mongolia" },
                new MyContact() { FullName = "Freddy Mercury", Gender = Gender.Male },
                new MyContact() { FullName = "Giordano Bruno", Gender = Gender.Male },
                new MyContact() { FullName = "Socrates", Gender = Gender.Male },
                new MyContact() { FullName = "Marie Curie", Gender = Gender.Female }
            };
        }
    }

    public class MyContact : INotifyPropertyChanged
    {
        private string _fullName;
        public string FullName { get { return _fullName; } set { _fullName = value; OnPropertyChanged("FullName"); } }

        private string _address;
        public string Address { get { return _address; } set { _address = value; OnPropertyChanged("Address"); } }

        private Gender _gender;
        public Gender Gender { get { return _gender; } set { _gender = value; OnPropertyChanged("Gender"); } }

        public event PropertyChangedEventHandler PropertyChanged;
        private void OnPropertyChanged(string propertyName)
        {
            if (this.PropertyChanged != null)
                PropertyChanged(this, new PropertyChangedEventArgs(propertyName));
        }
    }

    public enum Gender
    {
        Male,
        Female
    }

关于wpf - 将弹出窗口位置锁定到元素,或伪造带有图层的弹出窗口以在 ItemsControl 中进行就地编辑,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/15371659/

相关文章:

java - Selenium/WebDriver - 处理仅有时存在的弹出窗口?

javascript - 在其外部单击关闭 DIV 弹出窗口

html - z-index !important 对我的 div 没有任何作用

css - 在背景图片后面设置 Div

wpf - 如何在wpf工具包中动态添加更多线系列到折线图中?

wpf - 如何根据 ViewModel 属性值添加淡入/淡出动画?

python - kivy:带有进度条的弹出窗口

ios - SwiftUI View 和 UIKit View 的 z-index

C# WPF - 如何删除 DataGrid 中的列

c# - 类型或命名空间名称 'Deployment' 在命名空间 'System.Windows' 中不存在