如何在WPF UserControl中访问Canvas?

时间:2017-03-01 19:58:56

标签: wpf

我是使用WPF的新手,但非常喜欢在xaml中编写视图代码并在视图模型中支持代码的想法。我想要做的是扩展Canvas的使用,方法是将它与状态栏相关联,状态栏根据画布的内容和鼠标位置显示状态文本(下面的程式化代码不包括此内容)。

我的方法是创建一个包含Canvas的UserControl,并根据https://www.codeproject.com/Articles/82464/How-to-Embed-Arbitrary-Content-in-a-WPF-Control将ContentPresenter放入其中。

我有两个问题需要解决: 1)我需要做什么才能允许多个子控件与Canvas允许多个子控件相同的方式? 2)如何从主窗口代码访问Canvas的属性,例如Canvas.Left?

提前感谢您提出的任何建议。

UserControl xaml代码,后面的UserControl代码和主窗口xaml代码:

<UserControl x:Class="SO.CanvasUserControl"
             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:local="clr-namespace:SO"
             mc:Ignorable="d" 
             d:DesignHeight="300" d:DesignWidth="300">
  <UserControl.Template>
    <ControlTemplate TargetType="{x:Type local:CanvasUserControl}">
      <Canvas Width="200" Height="100" Background="Green">
        <ContentPresenter/>
      </Canvas>
    </ControlTemplate>
  </UserControl.Template>
</UserControl>

代码背后:

  public partial class CanvasUserControl : UserControl
  {
    public CanvasUserControl()
    {
      InitializeComponent();
    }
  }

主窗口:

<Window x:Class="SO.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
        xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
        xmlns:local="clr-namespace:SO"
        mc:Ignorable="d"
        Title="MainWindow" Height="350" Width="525">
    <Grid>

    <!--   works as expected
      <Canvas Width="200" Height="100" Background="Green">
      <Line X1="0" Y1="0" X2="200" Y2="100" Stroke="Red"/>
      <Line X1="200" Y1="0" X2="0" Y2="100" Stroke="Red"/>
    </Canvas>
  -->

    <!-- works as expected
     <Canvas Width="200" Height="100" Background="Green" x:Name="MyCanvas">
      <Line X1="{Binding ElementName=MyCanvas, Path=Left}" Y1="{Binding ElementName=MyCanvas, Path=Top}" X2="{Binding ElementName=MyCanvas, Path=ActualWidth}" Y2="{Binding ElementName=MyCanvas, Path=ActualHeight}" Stroke="Red"/>
      <Line X1="{Binding ElementName=MyCanvas, Path=ActualWidth}" Y1="{Binding ElementName=MyCanvas, Path=Top}" X2="{Binding ElementName=MyCanvas, Path=Left}" Y2="{Binding ElementName=MyCanvas, Path=ActualHeight}" Stroke="Red"/>
    </Canvas>
     -->


    <!-- How do I add more than one child control as nested content for the Canvas?
    <local:CanvasUserControl x:Name="MyCanvasUserControl">
      <Line X1="0" Y1="0" X2="200" Y2="100" Stroke="Red"/>
      <Line X1="200" Y1="0" X2="0" Y2="100" Stroke="Green"/>
    </local:CanvasUserControl>
    -->

    <!-- How do I access dependency properties of the Canvas?
    <local:CanvasUserControl x:Name="MyCanvasUserControl">
      <Line X1="{Binding ElementName=MyCanvasUserControl, Path=Left}" Y1="{Binding ElementName=MyCanvasUserControl, Path=Top}" X2="{Binding ElementName=MyCanvasUserControl, Path=ActualWidth}" Y2="{Binding ElementName=MyCanvasUserControl, Path=ActualHeight}" Stroke="Red"/>
    </local:CanvasUserControl>
    -->

  </Grid>
</Window>

2 个答案:

答案 0 :(得分:1)

我把它放在一起非常快,但它可以在WPF中运行并演示控制子类,加上ItemsControl自定义。

请注意,没有viewmodel。我们不需要一个,在自定义控件中我们不需要一个。 Viewmodels由消费者提供;当你编写自定义控件时,你正在冒充框架。自定义控件设计用于其他人的视图模型,因此必须单独留出DataContext。如果你不这样做,你会发现自己一直在发明变通办法。目标是编写一些与标准WPF控件完全相同的东西。

StatusBarCanvas.cs

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Controls.Primitives;
using System.Windows.Input;
using System.Windows.Media;

namespace CanvasUserControl
{
    public class StatusBarCanvas : ItemsControl
    {
        public StatusBarCanvas()
        {
        }

        static StatusBarCanvas()
        {
            //  This lets the framework finds the implicit style in 
            //  Themes\Generic.xaml (below)
            DefaultStyleKeyProperty.OverrideMetadata(
                typeof(StatusBarCanvas), 
                new FrameworkPropertyMetadata(typeof(StatusBarCanvas)));
        }

        public override void OnApplyTemplate()
        {
            base.OnApplyTemplate();

            var itemsPresenter = (ItemsPresenter)GetTemplateChild("PART_ItemsPresenter");

            //  The ItemsPresenter isn't ready yet right now, so do this 
            //  when it's Loaded.
            itemsPresenter.Loaded += (s,e) =>
            {
                Canvas = GetVisualChild<CustomCanvas>(itemsPresenter);
            };
        }

        #region Helpers
        //  Stolen from here: http://stackoverflow.com/a/7321019/424129
        private static T GetVisualChild<T>(DependencyObject parent) where T : Visual
        {
            T child = default(T);

            int numVisuals = VisualTreeHelper.GetChildrenCount(parent);

            for (int i = 0; i < numVisuals; i++)
            {
                Visual v = (Visual)VisualTreeHelper.GetChild(parent, i);

                //System.Diagnostics.Trace.WriteLine($"Visual Child {child}");

                child = v as T;

                if (child == null)
                {
                    child = GetVisualChild<T>(v);
                }
                if (child != null)
                {
                    break;
                }
            }
            return child;
        }

        #endregion Helpers

        #region Canvas Property
        /// <summary>
        /// Gets the canvas control.
        /// This is a readonly dependency property. Don't let anybody try to replace 
        /// the Canvas on us. 
        /// </summary>
        public CustomCanvas Canvas
        {
            get { return (CustomCanvas)GetValue(CanvasProperty); }
            protected set { SetValue(CanvasPropertyKey, value); }
        }

        internal static readonly DependencyPropertyKey CanvasPropertyKey =
            DependencyProperty.RegisterReadOnly("Canvas", typeof(CustomCanvas), 
                typeof(StatusBarCanvas), new PropertyMetadata(null));

        public static readonly DependencyProperty CanvasProperty = 
            CanvasPropertyKey.DependencyProperty;
        #endregion Canvas Property
    }

    public class CustomCanvas : Canvas
    {
        public CustomCanvas()
        {
            //  This event is only happening for me on enter/leave. 
            //  Fortunately that's not the question you're asking.
            MouseMove += CustomCanvas_MouseMove;
        }

        protected void UpdateMousePosition()
        {
            MousePosition = Mouse.GetPosition(this);

            System.Diagnostics.Trace.
                WriteLine($"MouseMove {DateTime.Now} Position {MousePosition}");
        }

        private void CustomCanvas_MouseMove(object sender, MouseEventArgs e)
        {
            UpdateMousePosition();
        }

        #region MousePosition Property
        /// <summary>
        /// Gets the mouse position relative to the Canvas
        /// Again, readonly. 
        /// </summary>

        public Point MousePosition
        {
            get { return (Point)GetValue(MousePositionProperty); }
            protected set { SetValue(MousePositionPropertyKey, value); }
        }
        internal static readonly DependencyPropertyKey MousePositionPropertyKey =
            DependencyProperty.RegisterReadOnly("MousePosition", 
                typeof(Point), typeof(CustomCanvas), new PropertyMetadata(null));

        public static readonly DependencyProperty MousePositionProperty = 
            MousePositionPropertyKey.DependencyProperty;
        #endregion MousePosition Property
    }
}

主题\ Generic.xaml

<ResourceDictionary 
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
    xmlns:local="clr-namespace:CanvasUserControl"
    >
    <Style TargetType="local:StatusBarCanvas">
        <Setter Property="Template">
            <Setter.Value>
                <ControlTemplate TargetType="local:StatusBarCanvas">
                    <Grid
                        Margin="{TemplateBinding Padding}"
                        >
                        <Grid.RowDefinitions>
                            <RowDefinition Height="*" />
                            <RowDefinition Height="Auto" />
                        </Grid.RowDefinitions>
                        <!-- The Border has no Row, so it fills the whole grid
                        behind the other controls -->
                        <Border
                            Background="{TemplateBinding Background}"
                            BorderBrush="{TemplateBinding BorderBrush}"
                            BorderThickness="{TemplateBinding BorderThickness}"
                            />
                        <ItemsPresenter 
                            x:Name="PART_ItemsPresenter"
                            Grid.Row="0"
                            />
                        <StatusBar 
                            Grid.Row="1"
                            x:Name="PART_StatusBar"
                            >
                            <StatusBarItem 
                                Content="{Binding Canvas.MousePosition, RelativeSource={RelativeSource TemplatedParent}}" 
                                ContentStringFormat="{}({0})"
                                />
                        </StatusBar>
                    </Grid>
                </ControlTemplate>
            </Setter.Value>
        </Setter>
        <Setter Property="ItemsPanel">
            <Setter.Value>
                <ItemsPanelTemplate>
                    <local:CustomCanvas
                        />
                </ItemsPanelTemplate>
            </Setter.Value>
        </Setter>
    </Style>
</ResourceDictionary>

MainWindow.xaml

<Grid>
    <local:StatusBarCanvas 
        x:Name="TestCanvas"
        Width="200"
        BorderBrush="Black"
        BorderThickness="1"
        >
        <Line 
            X1="0" Y1="0" 
            X2="{Binding Canvas.ActualHeight, ElementName=TestCanvas}"
            Y2="{Binding Canvas.ActualWidth, ElementName=TestCanvas}"
            Stroke="DeepSkyBlue"
            StrokeThickness="2"
            />
        <Line 
            X1="20" Y1="20" 
            X2="100"
            Y2="20"
            Stroke="OrangeRed"
            StrokeThickness="4"
            />
        <Label
            HorizontalAlignment="Center"
            VerticalAlignment="Bottom"
            Content="Foo Bar Planxty"
            Canvas.Left="100"
            Canvas.Top="100"
            />
    </local:StatusBarCanvas>
</Grid>

答案 1 :(得分:0)

也许我并没有完全理解你的问题,但听起来你只想拥有一个内置预定义状态栏的Canvas。您可以通过扩展Canvas而不是UserControl来轻松完成此操作。这是一个扩展Canvas并具有状态栏的自定义组件。

<Canvas x:Class="SO.CanvasUserControl"
             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:local="clr-namespace:SO"
             mc:Ignorable="d" 
             d:DesignHeight="300" d:DesignWidth="300">
    <Border Width="525" Height="30" Background="Black" Canvas.Bottom="0">
        <TextBlock Foreground="White" Text="Hello world" FontSize="16" />
    </Border>
</Canvas>

现在将其添加到您的主窗口并分配您喜欢的任何孩子。您将看到状态栏与所有子项一起显示。由于组件扩展了Canvas,您可以根据需要添加任意数量的子项,并且可以绑定到Canvas依赖项属性。

<Window x:Class="SO.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
        xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
        xmlns:local="clr-namespace:SO"
        mc:Ignorable="d"
        Title="MainWindow" Height="350" Width="525">

    <local:CanvasUserControl x:Name="MyCanvasUserControl" >
        <Line X1="0" Y1="0" X2="200" Y2="100" Stroke="Red"/>
        <Line X1="200" Y1="0" X2="0" Y2="100" Stroke="Green"/>
        <Line X1="{Binding ElementName=MyCanvasUserControl, Path=Left}" Y1="{Binding ElementName=MyCanvasUserControl, Path=Top}" X2="{Binding ElementName=MyCanvasUserControl, Path=ActualWidth}" Y2="{Binding ElementName=MyCanvasUserControl, Path=ActualHeight}" Stroke="Red"/>
    </local:CanvasUserControl>
</Window>