消息对话框是UI界面中不可或缺的组成部分,用于给用户一些提示,警告或者询问的窗口。在WPF中,消息对话框是系统原生(user32.dll)的MessageBox,无法通过Style或者Template来修改消息对话框的外观。因此,当需要一个与应用程序主题风格一致的消息对话框时,只能自己动手造轮子了。

确定“轮子”的功能

消息对话框的核心功能是向用户显示信息,并在用户对消息进行处理前中断用户的操作。根据常见的应用场景,可以梳理出以下几点功能:

  • 支持的消息类型:提示信息、警告信息、错误信息、询问信息
  • 支持的对话框类型:迷你模式(显示简要信息并自动关闭)、普通模式、完整模式(适用于消息内容分层级显示)
  • 设置消息对话框是否将触发源作为父窗体并显示遮罩层

    主要功能如下图所示:

开始造“轮子”

消息对话框本质也是一个窗体,因此首先要做的是自定义一个弹窗的样式,然后根据消息类型以及对话框类型定义相应的模板。

自定义窗口外观

标准的窗口由两个重叠的矩形组成。外部矩形是非工作区,其中包括标题栏按钮(最小化、最大化和关闭) 、窗口边框、调整大小和移动行为、应用程序图标和标题以及系统菜单。它由操作系统的窗口管理器绘制和管理。其尺寸由标准操作系统设置决定。内部矩形是工作区,也就是应用程序的内容。

自定义窗口外观主要是针对非工作区,可以通过设置属性WindowStyle为None,或者使用 WindowChrome类来自定义。这里我们使用前一种方法。

<!-- 弹出提示窗体模板 -->
<ControlTemplate x:Key="AlertDialogBaseTemplate" TargetType="{x:Type Window}">
<Border x:Name="border" Margin="0"
Background="White" CornerRadius="3"
RenderTransformOrigin="0.5,0.5">
<i:Interaction.Triggers>
<i:EventTrigger EventName="Loaded">
<helper:EventToCommand Command="{Binding LoadedCommand}" CommandParameter="{Binding RelativeSource={RelativeSource Mode=FindAncestor, AncestorType={x:Type Window}}}" />
</i:EventTrigger>
</i:Interaction.Triggers>
<Border.RenderTransform>
<TransformGroup>
<ScaleTransform />
</TransformGroup>
</Border.RenderTransform>
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="Auto" />
<RowDefinition Height="*" />
</Grid.RowDefinitions>
<toolkit:ImageButton Grid.Row="0" Width="16" Height="16"
Margin="0,16,16,0"
HorizontalAlignment="Right"
VerticalAlignment="Bottom"
Command="{Binding CloseWinCommand}"
CommandParameter="{Binding RelativeSource={RelativeSource Mode=FindAncestor, AncestorType={x:Type Window}}}"
DownImage="Images/AlterDialog/btnclose_hover.png"
HoverImage="Images/AlterDialog/btnclose_hover.png"
NormalImage="Images/AlterDialog/btnclose.png"
ToolTip="关闭"
Visibility="{Binding DialogMode, Converter={helper:EnumExcludeConverter}, ConverterParameter='Mini'}" />
<ContentPresenter Grid.Row="1" />
</Grid>
</Border>
</ControlTemplate> <!-- 弹出提示窗体样式 -->
<Style x:Key="AlterDailogBaseStyle" TargetType="{x:Type view:AlterDialogWindow}" BasedOn="{StaticResource BaseWindowStyle}">
<Setter Property="AllowsTransparency" Value="True" />
<Setter Property="Height" Value="180" />
<Setter Property="MaxHeight" Value="240" />
<Setter Property="MaxWidth" Value="400" />
<Setter Property="OverridesDefaultStyle" Value="True" />
<Setter Property="Template" Value="{StaticResource AlertDialogBaseTemplate}" />
<Setter Property="Topmost" Value="False" />
<Setter Property="Width" Value="400" />
<Setter Property="WindowState" Value="Normal" />
<Setter Property="WindowStyle" Value="None" />
</Style> <Style TargetType="{x:Type view:AlterDialogWindow}" BasedOn="{StaticResource AlterDailogBaseStyle}" />

上述代码中,通过把WindowStyle属性设置为None来隐藏默认的非工作区(控制区),然后再窗口的Template中定义一个两行的Grid,第一行模拟窗口非工作区的标题栏,本例中仅放一个关闭按钮。第二行则是工作区。

分享一个小小的经验:在定义AlterDialogWindow样式的时候,最后一行代码仅仅是定义了一个TargetType为view:AlterDialogWindow的样式,并且通过BasedOn继承自 x:Key="AlterDailogBaseStyle"的样式。这样做并非多此一举,而是为了方便局部需要个性化样式时最大限度地复用默认的全局样式。

自定义消息对话框模板

消息对话框整体可以划分为信息区域和交互区域两部分。信息区域呈现消息类型和消息内容,交互区域用于呈现确定和取消按钮。信息区域的布局及大小与对话框类型相关。交互区域则与消息类型以及对话框类型都有关。提示、警告、错误这三类消息是通知警示的作用,不需要用户做出YES or NO的处理,仅需要显示确定按钮即可,询问类信息则需要显示确定和取消两个按钮。迷你模式的对话框则不需显示确定和取消按钮,因此整个交互区都不显示。

根据三种类型的对话框定义三个信息区域的模板:

<DataTemplate x:Key="TemplateMini">
<StackPanel Margin="40,15,40,15" HorizontalAlignment="Center" Orientation="Horizontal">
<StackPanel.Resources>
<Style TargetType="{x:Type TextBlock}">
<Setter Property="FontSize" Value="18" />
<Setter Property="VerticalAlignment" Value="Center" />
</Style>
<Style TargetType="{x:Type toolkit:SelectableTextBlock}">
<Setter Property="FontSize" Value="18" />
<Setter Property="VerticalAlignment" Value="Center" />
</Style>
</StackPanel.Resources>
<Image Width="32" Height="34"
HorizontalAlignment="Right"
RenderOptions.BitmapScalingMode="LowQuality"
RenderOptions.CachingHint="Cache"
SnapsToDevicePixels="False"
Source="{Binding DialogType, Converter={StaticResource AlterDialogWindow_IconConverter}}"
Stretch="UniformToFill" />
<ScrollViewer MaxWidth="300" HorizontalScrollBarVisibility="Disabled" VerticalScrollBarVisibility="Auto">
<toolkit:SelectableTextBlock Margin="0,0,0,0"
HorizontalAlignment="Left" FontSize="18"
Foreground="#333333"
Text="{Binding Content}"
TextWrapping="Wrap" />
</ScrollViewer>
</StackPanel>
</DataTemplate> <DataTemplate x:Key="TemplateNormal">
<StackPanel Margin="40,18,40,0" HorizontalAlignment="Center" VerticalAlignment="Top" Orientation="Horizontal">
<StackPanel.Resources>
<Style TargetType="{x:Type TextBlock}">
<Setter Property="FontSize" Value="18" />
<Setter Property="VerticalAlignment" Value="Center" />
</Style>
<Style TargetType="{x:Type toolkit:SelectableTextBlock}">
<Setter Property="FontSize" Value="18" />
<Setter Property="VerticalAlignment" Value="Center" />
</Style>
</StackPanel.Resources>
<Image Width="40" Height="42"
HorizontalAlignment="Right"
RenderOptions.BitmapScalingMode="LowQuality"
RenderOptions.CachingHint="Cache"
SnapsToDevicePixels="False"
Source="{Binding DialogType, Converter={StaticResource AlterDialogWindow_IconConverter}}"
Stretch="UniformToFill" />
<ScrollViewer MaxWidth="280" HorizontalScrollBarVisibility="Disabled" VerticalScrollBarVisibility="Auto">
<toolkit:SelectableTextBlock Margin="0,0,0,0"
HorizontalAlignment="Left" FontSize="18"
Foreground="#333333"
Text="{Binding Content}"
TextWrapping="Wrap" />
</ScrollViewer>
</StackPanel>
</DataTemplate> <DataTemplate x:Key="TemplateFull">
<Grid Margin="40,10,40,0" HorizontalAlignment="Center" VerticalAlignment="Top">
<Grid.RowDefinitions>
<RowDefinition />
<RowDefinition />
</Grid.RowDefinitions>
<Image Width="54" Height="56"
HorizontalAlignment="Center"
RenderOptions.BitmapScalingMode="LowQuality"
RenderOptions.CachingHint="Cache"
SnapsToDevicePixels="False"
Source="{Binding DialogType, Converter={StaticResource AlterDialogWindow_IconConverter}}"
Stretch="UniformToFill" />
<ScrollViewer Grid.Row="1" MaxWidth="300"
Margin="0,12,0,0"
HorizontalScrollBarVisibility="Disabled"
VerticalScrollBarVisibility="Auto">
<StackPanel>
<toolkit:SelectableTextBlock Margin="0,0,0,0"
HorizontalAlignment="Center"
FontSize="18" Foreground="#333333"
Text="{Binding Content}"
TextWrapping="Wrap" />
<toolkit:SelectableTextBlock HorizontalAlignment="Center" FontSize="14" Foreground="#999999" Text="{Binding SubContent}" />
</StackPanel>
</ScrollViewer>
</Grid>
</DataTemplate>

交互区域可定义两个模板:仅显示确定按钮,显示确定和取消按钮。

<DataTemplate x:Key="Template0">
<StackPanel Orientation="Horizontal">
<toolkit:ImageButton Width="108" Height="56"
Command="{Binding YesCommand}"
DownImage="{Binding DialogType, Converter={StaticResource AlterDialogWindow_ButtonConverter}, ConverterParameter='0|2'}"
Foreground="{Binding DialogType, Converter={StaticResource AlterDialogWindow_ButtonConverter}, ConverterParameter='0|3'}"
HoverImage="{Binding DialogType, Converter={StaticResource AlterDialogWindow_ButtonConverter}, ConverterParameter='0|1'}"
NormalImage="{Binding DialogType, Converter={StaticResource AlterDialogWindow_ButtonConverter}, ConverterParameter='0|0'}">
<Grid>
<TextBlock FontSize="16" Foreground="White" Text="{Binding YesButtonText}" Visibility="{Binding IsCountdown, Converter={StaticResource VisibilityConverter}, ConverterParameter='!'}" />
<StackPanel Orientation="Horizontal" TextBlock.Foreground="White" Visibility="{Binding IsCountdown, Converter={StaticResource VisibilityConverter}}">
<TextBlock FontSize="16" Text="{Binding YesButtonText}" />
<TextBlock FontSize="14" Text="{Binding Countdown, StringFormat={}({0}s)}" />
</StackPanel>
</Grid>
</toolkit:ImageButton>
<toolkit:ImageButton Width="108" Height="32"
Margin="29,0,0,0"
Command="{Binding NoCommand}"
DownImage="{Binding DialogType, Converter={StaticResource AlterDialogWindow_ButtonConverter}, ConverterParameter='1|2'}"
Foreground="#366d85"
HoverImage="{Binding DialogType, Converter={StaticResource AlterDialogWindow_ButtonConverter}, ConverterParameter='1|1'}"
IsDefault="True"
NormalImage="{Binding DialogType, Converter={StaticResource AlterDialogWindow_ButtonConverter}, ConverterParameter='1|0'}">
<TextBlock FontSize="16" Foreground="#0099ff" Text="{Binding NoButtonText}" />
</toolkit:ImageButton> </StackPanel>
</DataTemplate> <DataTemplate x:Key="Template1">
<StackPanel Orientation="Horizontal">
<toolkit:ImageButton Width="108" Height="56"
Command="{Binding YesCommand}"
DownImage="{Binding DialogType, Converter={StaticResource AlterDialogWindow_ButtonConverter}, ConverterParameter='0|2'}"
FontSize="18"
Foreground="{Binding DialogType, Converter={StaticResource AlterDialogWindow_ButtonConverter}, ConverterParameter='0|3'}"
HoverImage="{Binding DialogType, Converter={StaticResource AlterDialogWindow_ButtonConverter}, ConverterParameter='0|1'}"
IsDefault="True"
NormalImage="{Binding DialogType, Converter={StaticResource AlterDialogWindow_ButtonConverter}, ConverterParameter='0|0'}">
<Grid>
<TextBlock FontSize="16" Foreground="White" Text="{Binding YesButtonText}" Visibility="{Binding IsCountdown, Converter={StaticResource VisibilityConverter}, ConverterParameter='!'}" />
<StackPanel Orientation="Horizontal" TextBlock.Foreground="White" Visibility="{Binding IsCountdown, Converter={StaticResource VisibilityConverter}}">
<TextBlock FontSize="16" Text="{Binding YesButtonText}" />
<TextBlock FontSize="14" Text="{Binding Countdown, StringFormat={}({0}s)}" />
</StackPanel>
</Grid>
</toolkit:ImageButton>
</StackPanel>
</DataTemplate>

定义好了信息区域和交互区域的几种模板后,AlterDialogWindow声明两个ContentPresenter表示信息区域和交互区域,通过模板选择器选择相应模板。其中交互区域通过绑定对话框类型来判断是否显示该区域。

<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="*" />
<RowDefinition Height="auto" />
</Grid.RowDefinitions>
<ContentPresenter HorizontalAlignment="Center" VerticalAlignment="Top" Content="{Binding}">
<ContentPresenter.ContentTemplateSelector>
<local:AlterDialogWindowContentTemplateSelector Template0="{StaticResource TemplateMini}" Template1="{StaticResource TemplateNormal}" Template2="{StaticResource TemplateFull}" />
</ContentPresenter.ContentTemplateSelector>
</ContentPresenter>
<ContentPresenter Grid.Row="1" Margin="0,0,0,16"
HorizontalAlignment="center"
VerticalAlignment="Top"
Content="{Binding}"
Visibility="{Binding DialogMode, Converter={helper:EnumExcludeConverter}, ConverterParameter='Mini'}">
<ContentPresenter.ContentTemplateSelector>
<local:AlterDialogWindowButtonDataTemplateSelector Template0="{StaticResource Template0}" Template1="{StaticResource Template1}" />
</ContentPresenter.ContentTemplateSelector>
</ContentPresenter>
</Grid>

至此,一个消息对话框就基本完成了。前边确定功能时提到调用消息对话框的窗口显示遮罩层。针对这个功能,我们可以在AlterDialogWindow中定义一个ShowDialog方法,参数是调用消息对话框的窗口对象,然后在该窗口中加上一个半透明的Grid作为遮罩层,并在AlterDialogWindow的OnClosed事件处理逻辑中删除遮罩层。

public bool? ShowDialog(DependencyObject parent)
{
if (this.Parent == null && parent != null)
{
Grid layer = new Grid() { Name = "maskLayer", Background = new SolidColorBrush(Color.FromArgb(128, 0, 0, 0)) };
_grid = Window.GetWindow(parent).FindFirstVisualChild<Grid>();
if (_grid.FindAllVisualChilds<Grid>().FirstOrDefault(r => r.Name == "maskLayer") == null)
_grid.Children.Add(layer);
if (_grid.RowDefinitions.Count > 0)
Grid.SetRowSpan(layer, _grid.RowDefinitions.Count);
if (_grid.ColumnDefinitions.Count > 0)
Grid.SetColumnSpan(layer, _grid.ColumnDefinitions.Count);
this.Owner = Window.GetWindow(parent);
this.WindowStartupLocation = WindowStartupLocation.CenterOwner;
}
return ShowDialog();
}

小结

本文介绍了自定义消息对话框的主要思路和代码,通过造轮子,重新温习了样式、主题、控件模板、数据模板、模板选择器、触发器、值转换器等技术。这也是MaterialDesign、HandyControl等控件珠玉在前,还要自己造轮子的原因之一。

[WPF]动手写一个简单的消息对话框的更多相关文章

  1. 动手写一个简单版的谷歌TPU-矩阵乘法和卷积

    谷歌TPU是一个设计良好的矩阵计算加速单元,可以很好的加速神经网络的计算.本系列文章将利用公开的TPU V1相关资料,对其进行一定的简化.推测和修改,来实际编写一个简单版本的谷歌TPU.计划实现到行为 ...

  2. 动手写一个简单版的谷歌TPU-指令集

    系列目录 谷歌TPU概述和简化 基本单元-矩阵乘法阵列 基本单元-归一化和池化(待发布) TPU中的指令集 SimpleTPU实例: (计划中) 拓展 TPU的边界(规划中) 重新审视深度神经网络中的 ...

  3. 动手写一个简单的Web框架(模板渲染)

    动手写一个简单的Web框架(模板渲染) 在百度上搜索jinja2,显示的大部分内容都是jinja2的渲染语法,这个不是Web框架需要做的事,最终,居然在Werkzeug的官方文档里找到模板渲染的代码. ...

  4. 动手写一个简单的Web框架(Werkzeug路由问题)

    动手写一个简单的Web框架(Werkzeug路由问题) 继承上一篇博客,实现了HelloWorld,但是这并不是一个Web框架,只是自己手写的一个程序,别人是无法通过自己定义路由和返回文本,来使用的, ...

  5. 动手写一个简单的Web框架(HelloWorld的实现)

    动手写一个简单的Web框架(HelloWorld的实现) 关于python的wsgi问题可以看这篇博客 我就不具体阐述了,简单来说,wsgi标准需要我们提供一个可以被调用的python程序,可以实函数 ...

  6. 自己动手写一个简单的(IIS)小型服务器

    因为第一次在博客园发表随笔,不太会用,这个笔记是我之前在印象笔记中写好的,然后直接copy过来,有兴趣自己做一个IIS服务器的小伙伴们可以参照下面的流程做一次,也可以叫我要源代码,不过要做完,我觉得花 ...

  7. 自己动手写一个简单的MVC框架(第一版)

    一.MVC概念回顾 路由(Route).控制器(Controller).行为(Action).模型(Model).视图(View) 用一句简单地话来描述以上关键点: 路由(Route)就相当于一个公司 ...

  8. 动手写一个简单版的谷歌TPU

    谷歌TPU是一个设计良好的矩阵计算加速单元,可以很好的加速神经网络的计算.本系列文章将利用公开的TPU V1(后简称TPU)相关资料,对其进行一定的简化.推测和修改,来实际编写一个简单版本的谷歌TPU ...

  9. 自己动手写一个简单的MVC框架(第二版)

    一.ASP.NET MVC核心机制回顾 在ASP.NET MVC中,最核心的当属“路由系统”,而路由系统的核心则源于一个强大的System.Web.Routing.dll组件. 在这个System.W ...

  10. 通过GUI制作一个简单的消息对话框互发消息

    public class LTS extends JFrame { private JPanel contentPane; private JTextField textField; private ...

随机推荐

  1. uni-app学习之路

    MVC模式1. model:模型层,数据的增删改查2. view:视图层,前端页面3. controller:控制层,处理业务 文件页面结构1. 页面以`.vue`结尾2. `template`,`s ...

  2. 在不修改代码情况下解决 Chrome 浏览器跨域

    前言: 在前后台分离的项目,跨域是经常遇到的问题了.是实际解决方案中,大部分采用服务端配置,而如果只是调试,可以通过配置 Chrome 浏览器实现跨域,以下以 NodeJs 服务为例. 开始: 1. ...

  3. python独立脚本应用Django项目的环境

    一.需求说明 一直用 Django 在开发一个网站项目,其中的注册用户和登录,都是使用Django自带的认证系统.主要是对密码的加密,在注册或者登录的时候,前端传递多来的密码,我会使用Django的s ...

  4. 淘宝详情api接口的应用

    淘宝详情API接口是一个基于HTTP协议的接口服务,可用于获取淘宝商品的具体信息.下面将介绍如何调用淘宝详情API接口获取淘宝商品数据的步骤. 1.注册账号并创建应用 首先,我们需要进行账号注册.实名 ...

  5. (洛谷P4213)杜教筛

    https://www.cnblogs.com/Mychael/p/8744633.html #pragma GCC optimize(3, "Ofast", "inli ...

  6. 请大家一定不要像我们公司这样打印log日志

    前言 最近接手了公司另一个项目,熟悉业务和代码苦不堪言. 我接手一个新项目,有个习惯,就是看结构,看数据库,搜代码. 其中搜代码是我个人这些年不知不觉形成的癖好,我下面给大家展示下这个小癖好. 正文 ...

  7. js原生 toggle函数编写

    工作中遇到了需要动态切换slide下拉框展示与隐藏,同时需要切换元素上附加的样式,以下脚本为实现此功能的实践. //元素点击时切换隐藏与展示逻辑 var slidsDownShow = documen ...

  8. Jenkins持续集成入门到精通(进阶篇)

    视频参考:https://www.bilibili.com/video/BV1Vp4y1b7ZN?p=51 1. Jenkins+Docker+SpringCloud持续集成流程说明 大致流程说明: ...

  9. Scrapy官方文档爬取

    最近想爬点啥东西看看, 所以接着学习了一点Scrapy, 学习过程中就试着去爬取Scrapy的官方文档作为练习之用, 现在已经基本完成了. 实现原理: 以 overview.html 为起点,通过 r ...

  10. Cplex求解教程(基于OPL语言,可作为大规模运算输入参考)

    最近导导让牛牛改篇论文,牛牛在她的指导下把非线性问题化成了线性.然鹅,化成线性后的模型决策变量和约束条件均达到上百甚至上千个,这让牛牛犯了难,以下方法或许能为这样大规模模型的变量和约束输入提供思路(๑ ...