WPF行为

行为是一款重用用户界面代码的更有挑战性的工具。其基本思想是:使用行为封装一些通用UI功能。如果具有适当的行为,可使用一两行XAML标记将其附加到任意元素,从而节省编写和调试代码的工作。

样式提供了重用一组属性设置的实用方法。它们帮助构建一致的、组织良好的界面迈出了重要的第一步------但它们还有许多限制。

问题是在典型的应用程序中,属性设置仅是用户界面基础结构的一小部分。甚至最基本的程序通常也需要大量的用户界面代码,这些代码与应用程序的功能无关。在许多程序中,用于用户界面任务的代码(如驱动动画、实现平滑效果、维护用户界面状态,以及支持诸如拖放、缩放以及停靠等用户急么特性),无论是在数量上还是复杂性上都超出了业务代码。许多这类代码是通用的,这意味着在创建每个WPF对象中需要编写相同的内容。所有这些工作几乎都是单调乏味的。

为回应这一挑战,ExpressionBlend创作者开发了称为行为(Behavior)的特征。其思想很简单:创建封装了一些通用用户界面功能的行为,这一功能可以是基本功能(如启动故事板或导航到超链接),也可以是复杂功能(如处理多点触摸交互,或构件使用实时物理引擎的碰撞模型)。一旦构建功能,就可将他们添加到任意应用程序的另一个控件中,具体方法是将该控件链接到适当的行为并设置行为的属性。

自定义控件时另一个在应用程序中(或在多个应用程序之间)重用用户界面功能的技术。然而,自定义控件必须作为可视化内容和代码的紧密链接包进行创建。尽管自定义控件非常强大,但却不能适应于需要大量具有类似功能的不同控件的情况(例如,为一组不同的元素添加鼠标悬停渲染效果)。因此,样式、行为以及自定义控件都是互补的。

获取行为支持

有一个问题,重用用户界面代码通用块的基础结构不是WPF的一部分。反而,它被捆绑到ExpressionBlend。这是因为行为开始是作为ExpressionBlend的设计时特性引入的。事实上,ExpressionBlend仍是通过将行为拖动到需要行为的控件上来添加行为的唯一工具。但这并不意味着行为只能用于ExpressionBlend,只需要付出很少的努力就可以在VisualStudio应用程序中创建和使用行为。只需要手动编写标记,而不是使用工具箱。

为了获得支持行为的程序集,有两种选择:

1、可安装Expression Blend或Expression Blend For Visual Studio ,所有这些版本都包含Visual Studio中的行为功能所需的程序集,但您只能通关Expression Blend For Visual Studio 在Blend环境中创建和编辑WPF应用程序。

2、可安装 Expression Blend SDK

无论是使用Expression Blend IDE还是SDK,最终要使用的两个程序集是:

System.Windows.Interactivity.dll 这个程序集定义了支持行为的基本类。它是行为特征的基础

Microsoft.Expression.Interactions.dll 这个程序集通过添加可选的以核心行为类为基础的动作和触发器类,增加了一些有用的扩展。

理解行为模型

行为特性具有两个版本,一个版本旨在为Silverlight 添加行为支持,Silverlight 是Microsoft 的针对浏览器的富客户端插件;而另一个版本是针对WPF设计的。尽管这两个版本提供了相同的特性,但行为特性和Silverlight 领域更吻合,因为它弥补了更大的鸿沟。与WPF不同,Silverlight 不支持触发器,所以实现行为的程序集也实现触发器更合理。然而,WPF支持触发器,行为特性包含自己的触发器系统,而触发器系统与WPF模型不匹配,这确实令人感到有些困惑。

问题在于具有类似名称的这两个特性有部分重合但不完全相同。在WPF中,触发器最重要的角色是构建灵活的样式和控件模板。在触发器的帮助下,样式和模板变得更加智能;例如,当一些属性发生变化时可视化效果。然而,ExpressionBlend中的触发器具有不同的目的。通过使用可视化设计工具,允许您为应用程序添加简单功能。换句话说,WPF触发器支持更强大的样式和控件模板。而ExpressionBlend触发器支持快速的不需要代码的应用程序设计。

那么,对于使用WPF的普通开发人员来说所有这些意味着什么呢?下面是几条指导原则:

1、行为模型不是WPF的核心部分,所以行为不像样式和模板那样确定。换句话说,可编写不使用行为的WPF应用程序,但如果不是样式和模板,就不能创建比"Hello World" 演示更复杂的WPF应用程序。

2、如果在ExpressionBlend上耗费大量时间,或希望为其他ExpressionBlend用户开发组件,您可能会对ExpressionBlend中的触发器特性感兴趣。尽管和WPF中的触发器系统使用相同的名称,但它们不相互重叠,您可以同时使用这两者。

3、如果不使用ExpressionBlend,可完全略过其触发器特性------但仍应分析ExpressionBlend提供的功能完整的行为类。这是因为行为比ExpressionBlend的触发器更强大也更常用。

创建行为

行为旨在封装一些UI功能,从而可以不必编写代码就能够将其应用到元素上。从另一个角度看,每个行为都为元素提供了一个服务。该服务通常涉及监听几个不同的事件并执行几个相关的操作。例如,为文本框提供水印,如果文本框为空,并且当前没有焦点,那么会以清淡的字体显示提示信息。当文本框具有焦点时,启动行为并删除水印文本。

为更好的理解行为,最好自己创建一个行为。这里实现一个为任意元素提供使用鼠标在Canvas面板上拖动元素的功能。对于单个元素实现该功能的基本步骤是非常简单的------代码监听鼠标事件并修改设置相应的Canvas坐标的附加属性。但通过付出更多一点的努力,可将该代码转换为可重用的行为,该行为可为Canvas面板上的所有元素提供拖动支持。

在任何行为中,第一步是覆盖OnAttached() 和 OnDetaching() 方法。当调用OnAttached() 方法时,可通过 AssociatedObject 属性访问放置行为的元素,并可关联事件处理程序。当调用 OnDetaching() 方法时,移除事件处理程序。

cs 复制代码
public class DragInCanvasBehavior : Behavior<UIElement>
{
    private Canvas? canvas;
    protected override void OnAttached()
    {
        base.OnAttached();
        // Hook up event handlers.            
        this.AssociatedObject.MouseLeftButtonDown += AssociatedObject_MouseLeftButtonDown;
        this.AssociatedObject.MouseMove += AssociatedObject_MouseMove;
        this.AssociatedObject.MouseLeftButtonUp += AssociatedObject_MouseLeftButtonUp;
    }
    protected override void OnDetaching()
    {
        base.OnDetaching();
        // Detach event handlers.
        this.AssociatedObject.MouseLeftButtonDown -= AssociatedObject_MouseLeftButtonDown;
        this.AssociatedObject.MouseMove -= AssociatedObject_MouseMove;
        this.AssociatedObject.MouseLeftButtonUp -= AssociatedObject_MouseLeftButtonUp;
    }
    // Keep track of when the element is being dragged.
    private bool isDragging = false;
    // When the element is clicked, record the exact position
    // where the click is made.
    private Point mouseOffset;
    private void AssociatedObject_MouseLeftButtonDown(object sender, MouseButtonEventArgs e)
    {
        // Find the canvas.
        if (canvas == null)
            canvas = VisualTreeHelper.GetParent(this.AssociatedObject) as Canvas;
        // Dragging mode begins.
        isDragging = true;
        // Get the position of the click relative to the element
        // (so the top-left corner of the element is (0,0).
        mouseOffset = e.GetPosition(AssociatedObject);
        // Capture the mouse. This way you'll keep receiveing
        // the MouseMove event even if the user jerks the mouse
        // off the element.
        AssociatedObject.CaptureMouse();
    }
    private void AssociatedObject_MouseMove(object sender, MouseEventArgs e)
    {
        if (isDragging)
        {
            // Get the position of the element relative to the Canvas.
            Point point = e.GetPosition(canvas);
            // Move the element.
            AssociatedObject.SetValue(Canvas.TopProperty, point.Y - mouseOffset.Y);
            AssociatedObject.SetValue(Canvas.LeftProperty, point.X - mouseOffset.X);
        }
    }
    private void AssociatedObject_MouseLeftButtonUp(object sender, MouseButtonEventArgs e)
    {
        if (isDragging)
        {
            AssociatedObject.ReleaseMouseCapture();
            isDragging = false;
        }
    }
}

DragInCanvasBehavior 类在OnAttached()方法中为 MouseLeftButtonDown、MouseMove、MouseLeftButtonUp 事件添加事件处理程序,在OnDetaching()方法中移除这些事件处理程序。这里通过AssociatedObject 获得此行为附加到的对象,并通过可视化树查找附加对象的父元素来获得Canvas对象。

使用行为

为使用该行为,只需要使用 Interaction.Behaviors 附加属性在Canvas面板中添加任意元素。这里添加了三个形状(其中的一个Ellipse没有添加行为)、一个TextBlock、一个Button。测试下来发现除了没有添加行为的Ellipse无法拖动外,Button也无法拖动。这是因为Button中的MouseLeftButtonDown和MouseLeftButtonUp事件无法触发,Button本身响应Click事件,相当于将MouseLeftButtonDown和MouseLeftButtonUp事件抑制了,转换成了Click事件。

cs 复制代码
<Canvas Height="200">
    <Rectangle Canvas.Left="10" Canvas.Top="10" Fill="Yellow" Width="40" Height="60">
        <i:Interaction.Behaviors>
            <local:DragInCanvasBehavior></local:DragInCanvasBehavior>
        </i:Interaction.Behaviors>
    </Rectangle>
    <Ellipse Canvas.Left="10" Canvas.Top="70" Fill="Blue" Width="80" Height="60"></Ellipse>
    <Ellipse Canvas.Left="80" Canvas.Top="70" Fill="OrangeRed" Width="40" Height="70">
        <i:Interaction.Behaviors>
            <local:DragInCanvasBehavior></local:DragInCanvasBehavior>
        </i:Interaction.Behaviors>
    </Ellipse>
    <TextBlock Text="TestBlock" Canvas.Left="280" Canvas.Top="170" Width="100" Height="30">
        <i:Interaction.Behaviors>
            <local:DragInCanvasBehavior></local:DragInCanvasBehavior>
        </i:Interaction.Behaviors>
    </TextBlock>
    <Button Content="TestButton" Canvas.Left="180" Canvas.Top="170" Width="100" Height="30">
        <i:Interaction.Behaviors>
            <local:DragInCanvasBehavior></local:DragInCanvasBehavior>
        </i:Interaction.Behaviors>
    </Button>
</Canvas>

使用行为触发器

行为触发器通常是继承自 System.Windows.Interactivity.TriggerAction类 或是 System.Windows.Interactivity.TargetedTriggerAction类。主要实现Invoke函数用来响应触发器事件。

cs 复制代码
public class FadeOutAction : TargetedTriggerAction<UIElement>
{
    // The default fade out time is 2 seconds.
    public static readonly DependencyProperty DurationProperty = 
        DependencyProperty.Register("Duration", typeof(TimeSpan), typeof(FadeOutAction), new PropertyMetadata(TimeSpan.FromSeconds(2)));

    public TimeSpan Duration
    {
        get { return (TimeSpan)GetValue(FadeOutAction.DurationProperty); }
        set { SetValue(FadeOutAction.DurationProperty, value); }
    }
    private Storyboard fadeStoryboard = new Storyboard();
    private DoubleAnimation fadeAnimation = new DoubleAnimation();
    public FadeOutAction()
    {
        fadeStoryboard.Children.Add(fadeAnimation);
    }
    protected override void Invoke(object args)
    {
        // Make sure the storyboard isn't already running.
        fadeStoryboard.Stop();
        // Set up the storyboard.            
        Storyboard.SetTargetProperty(fadeAnimation, new PropertyPath("Opacity"));
        Storyboard.SetTarget(fadeAnimation, this.Target);
        // Set up the animation.
        // It's important to do this at the last possible instant,
        // in case the value for the Duration property changes.
        fadeAnimation.To = 0;
        fadeAnimation.Duration = Duration;
        fadeStoryboard.Begin();
    }
}
public class FadeInAction : TargetedTriggerAction<UIElement>
{
    // The default fade in is 0.5 seconds.
    public static readonly DependencyProperty DurationProperty = 
        DependencyProperty.Register("Duration", typeof(TimeSpan), typeof(FadeInAction), new PropertyMetadata(TimeSpan.FromSeconds(0.5)));
    public TimeSpan Duration
    {
        get { return (TimeSpan)GetValue(FadeInAction.DurationProperty); }
        set { SetValue(FadeInAction.DurationProperty, value); }
    }
    private Storyboard fadeStoryboard = new Storyboard();
    private DoubleAnimation fadeAnimation = new DoubleAnimation();
    public FadeInAction()
    {
        fadeStoryboard.Children.Add(fadeAnimation);
    }
    protected override void Invoke(object args)
    {
        // Make sure the storyboard isn't already running.
        fadeStoryboard.Stop();
        // Set up the storyboard.                        
        Storyboard.SetTargetProperty(fadeAnimation, new PropertyPath("Opacity"));
        Storyboard.SetTarget(fadeAnimation, this.Target);
        // Set up the animation.            
        fadeAnimation.To = 1;
        fadeAnimation.Duration = Duration;
        fadeStoryboard.Begin();
    }
}

使用行为触发器则需要使用 Interaction.Triggers 附加属性,在其中添加触发器:

cs 复制代码
<StackPanel>
    <StackPanel Orientation="Horizontal" Margin="3,15">
        <Button Content="Click to Fade the Label" Padding="5">
            <i:Interaction.Triggers>
                <i:EventTrigger EventName="Click">
                    <local:FadeOutAction TargetName="border"  />
                </i:EventTrigger>
            </i:Interaction.Triggers>
        </Button>
        <Button Content="Click to Show the Label" Padding="5">
            <i:Interaction.Triggers>
                <i:EventTrigger EventName="Click">
                    <local:FadeInAction TargetName="border"  />
                </i:EventTrigger>
            </i:Interaction.Triggers>
        </Button>
    </StackPanel>
    <Border x:Name="border" Background="Orange" BorderBrush="Black" BorderThickness="1" Margin="3,0" Opacity="0.5">
        <TextBlock Margin="5" FontSize="17" TextWrapping="Wrap"  Text="I'm the target of the FadeOutAction and FadeInAction."></TextBlock>
    </Border>
</StackPanel>

完整的测试代码如下:

MainWindow.xaml

cs 复制代码
<Window x:Class="TestBehavior.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:TestBehavior"
        xmlns:i="clr-namespace:System.Windows.Interactivity;assembly=System.Windows.Interactivity" 
        mc:Ignorable="d"
        Title="MainWindow" Height="500" Width="800">
    <StackPanel>
        <Canvas Height="200">
            <Rectangle Canvas.Left="10" Canvas.Top="10" Fill="Yellow" Width="40" Height="60">
                <i:Interaction.Behaviors>
                    <local:DragInCanvasBehavior></local:DragInCanvasBehavior>
                </i:Interaction.Behaviors>
            </Rectangle>
            <Ellipse Canvas.Left="10" Canvas.Top="70" Fill="Blue" Width="80" Height="60"></Ellipse>
            <Ellipse Canvas.Left="80" Canvas.Top="70" Fill="OrangeRed" Width="40" Height="70">
                <i:Interaction.Behaviors>
                    <local:DragInCanvasBehavior></local:DragInCanvasBehavior>
                </i:Interaction.Behaviors>
            </Ellipse>
            <TextBlock Text="TestBlock" Canvas.Left="280" Canvas.Top="170" Width="100" Height="30">
                <i:Interaction.Behaviors>
                    <local:DragInCanvasBehavior></local:DragInCanvasBehavior>
                </i:Interaction.Behaviors>
            </TextBlock>
            <Button Content="TestButton" Canvas.Left="180" Canvas.Top="170" Width="100" Height="30">
                <i:Interaction.Behaviors>
                    <local:DragInCanvasBehavior></local:DragInCanvasBehavior>
                </i:Interaction.Behaviors>
            </Button>
        </Canvas>
        <StackPanel>
            <StackPanel Orientation="Horizontal" Margin="3,15">
                <Button Content="Click to Fade the Label" Padding="5">
                    <i:Interaction.Triggers>
                        <i:EventTrigger EventName="Click">
                            <local:FadeOutAction TargetName="border"  />
                        </i:EventTrigger>
                    </i:Interaction.Triggers>
                </Button>
                <Button Content="Click to Show the Label" Padding="5">
                    <i:Interaction.Triggers>
                        <i:EventTrigger EventName="Click">
                            <local:FadeInAction TargetName="border"  />
                        </i:EventTrigger>
                    </i:Interaction.Triggers>
                </Button>
            </StackPanel>
            <Border x:Name="border" Background="Orange" BorderBrush="Black" BorderThickness="1" Margin="3,0" Opacity="0.5">
                <TextBlock Margin="5" FontSize="17" TextWrapping="Wrap"  Text="I'm the target of the FadeOutAction and FadeInAction."></TextBlock>
            </Border>
        </StackPanel>
        <StackPanel>
            <Button Content="Click to Play Sound" HorizontalAlignment="Left" Padding="5" Margin="3">
                <i:Interaction.Triggers>
                    <i:EventTrigger EventName="Click">
                        <local:PlaySoundAction Source="test.mp3" />
                    </i:EventTrigger>
                </i:Interaction.Triggers>
            </Button>
        </StackPanel>
    </StackPanel>
</Window>

MainWindow.xaml.cs

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.Data;
using System.Windows.Documents;
using System.Windows.Input;
using System.Windows.Interactivity;
using System.Windows.Media;
using System.Windows.Media.Animation;
using System.Windows.Media.Imaging;
using System.Windows.Navigation;
using System.Windows.Shapes;

namespace TestBehavior;

public class DragInCanvasBehavior : Behavior<UIElement>
{
    private Canvas? canvas;
    protected override void OnAttached()
    {
        base.OnAttached();
        // Hook up event handlers.            
        this.AssociatedObject.MouseLeftButtonDown += AssociatedObject_MouseLeftButtonDown;
        this.AssociatedObject.MouseMove += AssociatedObject_MouseMove;
        this.AssociatedObject.MouseLeftButtonUp += AssociatedObject_MouseLeftButtonUp;
    }
    protected override void OnDetaching()
    {
        base.OnDetaching();
        // Detach event handlers.
        this.AssociatedObject.MouseLeftButtonDown -= AssociatedObject_MouseLeftButtonDown;
        this.AssociatedObject.MouseMove -= AssociatedObject_MouseMove;
        this.AssociatedObject.MouseLeftButtonUp -= AssociatedObject_MouseLeftButtonUp;
    }
    // Keep track of when the element is being dragged.
    private bool isDragging = false;
    // When the element is clicked, record the exact position
    // where the click is made.
    private Point mouseOffset;
    private void AssociatedObject_MouseLeftButtonDown(object sender, MouseButtonEventArgs e)
    {
        // Find the canvas.
        if (canvas == null)
            canvas = VisualTreeHelper.GetParent(this.AssociatedObject) as Canvas;
        // Dragging mode begins.
        isDragging = true;
        // Get the position of the click relative to the element
        // (so the top-left corner of the element is (0,0).
        mouseOffset = e.GetPosition(AssociatedObject);
        // Capture the mouse. This way you'll keep receiveing
        // the MouseMove event even if the user jerks the mouse
        // off the element.
        AssociatedObject.CaptureMouse();
    }
    private void AssociatedObject_MouseMove(object sender, MouseEventArgs e)
    {
        if (isDragging)
        {
            // Get the position of the element relative to the Canvas.
            Point point = e.GetPosition(canvas);
            // Move the element.
            AssociatedObject.SetValue(Canvas.TopProperty, point.Y - mouseOffset.Y);
            AssociatedObject.SetValue(Canvas.LeftProperty, point.X - mouseOffset.X);
        }
    }
    private void AssociatedObject_MouseLeftButtonUp(object sender, MouseButtonEventArgs e)
    {
        if (isDragging)
        {
            AssociatedObject.ReleaseMouseCapture();
            isDragging = false;
        }
    }
}
public class FadeOutAction : TargetedTriggerAction<UIElement>
{
    // The default fade out time is 2 seconds.
    public static readonly DependencyProperty DurationProperty = 
        DependencyProperty.Register("Duration", typeof(TimeSpan), typeof(FadeOutAction), new PropertyMetadata(TimeSpan.FromSeconds(2)));

    public TimeSpan Duration
    {
        get { return (TimeSpan)GetValue(FadeOutAction.DurationProperty); }
        set { SetValue(FadeOutAction.DurationProperty, value); }
    }
    private Storyboard fadeStoryboard = new Storyboard();
    private DoubleAnimation fadeAnimation = new DoubleAnimation();
    public FadeOutAction()
    {
        fadeStoryboard.Children.Add(fadeAnimation);
    }
    protected override void Invoke(object args)
    {
        // Make sure the storyboard isn't already running.
        fadeStoryboard.Stop();
        // Set up the storyboard.            
        Storyboard.SetTargetProperty(fadeAnimation, new PropertyPath("Opacity"));
        Storyboard.SetTarget(fadeAnimation, this.Target);
        // Set up the animation.
        // It's important to do this at the last possible instant,
        // in case the value for the Duration property changes.
        fadeAnimation.To = 0;
        fadeAnimation.Duration = Duration;
        fadeStoryboard.Begin();
    }
}
public class FadeInAction : TargetedTriggerAction<UIElement>
{
    // The default fade in is 0.5 seconds.
    public static readonly DependencyProperty DurationProperty = 
        DependencyProperty.Register("Duration", typeof(TimeSpan), typeof(FadeInAction), new PropertyMetadata(TimeSpan.FromSeconds(0.5)));
    public TimeSpan Duration
    {
        get { return (TimeSpan)GetValue(FadeInAction.DurationProperty); }
        set { SetValue(FadeInAction.DurationProperty, value); }
    }
    private Storyboard fadeStoryboard = new Storyboard();
    private DoubleAnimation fadeAnimation = new DoubleAnimation();
    public FadeInAction()
    {
        fadeStoryboard.Children.Add(fadeAnimation);
    }
    protected override void Invoke(object args)
    {
        // Make sure the storyboard isn't already running.
        fadeStoryboard.Stop();
        // Set up the storyboard.                        
        Storyboard.SetTargetProperty(fadeAnimation, new PropertyPath("Opacity"));
        Storyboard.SetTarget(fadeAnimation, this.Target);
        // Set up the animation.            
        fadeAnimation.To = 1;
        fadeAnimation.Duration = Duration;
        fadeStoryboard.Begin();
    }
}

[DefaultTrigger(typeof(ButtonBase), typeof(System.Windows.Interactivity.EventTrigger), new object[] { "Click" })]
[DefaultTrigger(typeof(Shape), typeof(System.Windows.Interactivity.EventTrigger), new object[] { "MouseEnter" })]
[DefaultTrigger(typeof(UIElement), typeof(System.Windows.Interactivity.EventTrigger), new object[] { "MouseLeftButtonDown" })]
public class PlaySoundAction : TriggerAction<FrameworkElement>
{
    public static readonly DependencyProperty SourceProperty = DependencyProperty.Register("Source", typeof(Uri), typeof(PlaySoundAction), new PropertyMetadata(null));
    public Uri Source
    {
        get { return (Uri)GetValue(PlaySoundAction.SourceProperty); }
        set { SetValue(PlaySoundAction.SourceProperty, value); }
    }
    protected override void Invoke(object args)
    {
        // Find a place to insert the MediaElement.
        Panel? container = FindContainer();
        if (container != null)
        {
            // Create and configure the MediaElement.
            MediaElement media = new MediaElement();
            media.Source = this.Source;
            // Hook up handlers that will clean up when playback finishes.
            media.MediaEnded += delegate
            {
                container.Children.Remove(media);
            };
            media.MediaFailed += delegate
            {
                container.Children.Remove(media);
            };
            // Add the MediaElement and begin playback.                
            container.Children.Add(media);
        }
    }
    private Panel? FindContainer()
    {
        FrameworkElement? element = this.AssociatedObject;
        // Search for some sort of panel where the MediaElement can be inserted.            
        while (element != null)
        {
            if (element is Panel)
                return (Panel)element;
            element = VisualTreeHelper.GetParent(element) as FrameworkElement;
        }
        return null;
    }
}

/// <summary>
/// Interaction logic for MainWindow.xaml
/// </summary>
public partial class MainWindow : Window
{
    public MainWindow()
    {
        InitializeComponent();
    }
}
相关推荐
月落.6 小时前
WPF的<ContentControl>控件
wpf
就是有点傻6 小时前
WPF中的依赖属性
开发语言·wpf
wangnaisheng6 小时前
【WPF】把一个Window放在左上角/右上角顶格显示
wpf
WineMonk6 小时前
.NET WPF CommunityToolkit.Mvvm框架
.net·wpf·mvvm
月落.6 小时前
WPF中的INotifyPropertyChanged接口
wpf
界面开发小八哥6 小时前
界面控件DevExpress WPF中文教程:Data Grid——卡片视图设置
.net·wpf·界面控件·devexpress·ui开发
平凡シンプル6 小时前
WPF 打包
wpf
VickyJames6 小时前
基于XAML框架和跨平台项目架构设计的深入技术分析
wpf·开源分享·unoplatform·winui3·项目架构
冷眼Σ(-᷅_-᷄๑)10 小时前
WPF缩放动画和平移动画叠加后会发生什么?
wpf·动画
△曉風殘月〆12 小时前
WPF MVVM入门系列教程(二、依赖属性)
c#·wpf·mvvm