将弹出位置锁定到元素,或伪造带有图层的弹出窗口,以便在ItemsControl中进行就地编辑 [英] Locking popup position to element, or faking a popup with layers for in-place editing in an ItemsControl

查看:103
本文介绍了将弹出位置锁定到元素,或伪造带有图层的弹出窗口,以便在ItemsControl中进行就地编辑的处理方法,对大家解决问题具有一定的参考价值,需要的朋友们下面随着小编来一起学习吧!

问题描述

我想要实现的基本上是在wpf中的ItemsControl中对数据绑定对象进行就地编辑.

What I am trying to achieve is essentially in-place editing of a databound object inside an ItemsControl in wpf.

my ItemsControl是水平的WrapPanel,其中包含用户控件(NameControl)的多个实例,该实例显示为带有人名的粉红色小字形.看起来像这样

my ItemsControl is a horizontal WrapPanel containing multiple instances of a usercontrol (NameControl) which displays as a little pink Glyph with a person's name. It looks like this

使用弹出窗口,我可以显示该名称"的编辑器(绑定对象的其他属性,例如AddressGender等),这绝对可以正常工作.这时我的XAML符合

With a popup I am able to show an editor for this "Name" (Other properties of the bound object things like Address,Gender etc.) and this works absoluttely fine. My XAML at this point would be along the lines of

<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>

当我单击一个看起来像的名称时提供输出

Giving an output when I click a Name looking like

除了弹出框不随寡妇移动(调整大小/最小化/最大化)而且弹出框位于一切之上之外,这种外观让我非常高兴(除了我糟糕的颜色选择!!)甚至其他窗户.

With this look im quite happy (apart from my awful choice of colours!!) except that the popup does not move with the widow (resize/minimize/maximize) and that popup is above everything even other windows.

所以解决部分问题的一种方法是附加"或将弹出位置锁定到元素.我还没有找到一种好的/简单/xml的方法来做到这一点.香港专业教育学院遇到了一些基于代码的解决方案,但我不确定我是否喜欢那样.只是有一点气味.

So one way to solve part of that is to "attach" or lock the popup position to the element. I have not found a good/easy/xaml way to do that. Ive come across a few code-based solutions but im not sure I like that. It just has a bit of a smell about it.

本人试图实现的另一种解决方案是抛弃弹出窗口,并尝试模拟位于其他名称上方但位于相关名称控件上方(或位于下方)的图层/面板的行为.

Another solution ive tried to achieve is to ditch the popup and try to emulate the behaviour of a layer/panel that sits above the other names but is position over (or below, im not fussy) the associated name control.

我尝试了一些不同的操作,主要是在PanelControl(位于MainWindow顶部的Grid,WrapPanel,DockPanel)内的控件上设置Panel.ZIndex,但收效甚微.我已经实现了一个简单的BoolToVisibilityConverter来将我的编辑器Grid的Visibility属性绑定到我的IsEditMode视图模型属性,并且效果很好,但是我一生都无法将我的元素安排在ItemsControl中以显示编辑器网格覆盖名称.

Ive tried a few different things, mainly around setting Panel.ZIndex on controls within a PanelControl (The Grid, the WrapPanel, a DockPanel on the very top of my MainWindow) with little success. I have implemented a simple BoolToVisibilityConverter to bind my editor Grid's Visibility property to my IsEditMode view model property and that works fine, but I cant for the life of me arrange my elements in the ItemsControl to show the editor grid over the names.

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

To do what is described above I simply commented out the Popup and added the following binding to the Border which contains the editor grid Visibility="{Binding IsEditMode, Converter={StaticResource boolToVisibility}}".

所有操作就是这样:

它仅显示弹出窗口下,而在其他名字上上,而不是 .

It just shows the popup under the name but not over the others.

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

Any help? What am I doing wrong?

推荐答案

听起来像是我的AdornerLayer的工作.

Sounds like a job for the AdornerLayer to me.

我的实现一次只显示一个弹出窗口",您可以通过再次单击按钮将其隐藏.但是,您也可以在ContactAdorner上添加一个小的关闭按钮,或按OK按钮,或在IsAitner后面的IsoritLayer中填充IsHitTestVisible元素,并通过隐藏打开的Adorner来对单击做出反应(因此单击外部任何位置都会关闭弹出窗口)

My implementation will just display one 'popup' at a time, and you can hide it by clicking the button another time. But you could also add a small close button to the ContactAdorner, or stick with your OK button, or fill the AdornerLayer behind the ContactAdorner with an element that IsHitTestVisible and reacts on click by hiding the open Adorner (so clicking anywhere outside closes the popup).

根据您的要求添加了小的关闭按钮. ContactAdorner和ContactDetailsTemplate中的更改.

您可能要添加的另一件事是,将装饰器从底部剪下后重新定位(我仅从右侧检查剪切).

Another thing that you might want to add is repositioning of the adorner once it is clipped from the bottom (I only check for clipping from the right).

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:

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;
        }
    }
}

ViewModel,MyContact:

ViewModel, MyContact:

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
    }

这篇关于将弹出位置锁定到元素,或伪造带有图层的弹出窗口,以便在ItemsControl中进行就地编辑的文章就介绍到这了,希望我们推荐的答案对大家有所帮助,也希望大家多多支持IT屋!

查看全文
登录 关闭
扫码关注1秒登录
发送“验证码”获取 | 15天全站免登陆