今天有群友在群里問 C# 能不能在 Linux 下訪問 Access資料庫? 我覺得這很有趣,因此研究折騰了一下,也因為很久沒有寫博文了,所以特意上來寫博文分享經驗。 運行環境 操作系統:Ubuntu 22.04.3 LTS (Jammy) 開發工具:Visual Studio 2022 (17.8 ...
消息對話框是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等控制項珠玉在前,還要自己造輪子的原因之一。
代碼示例
https://github.com/czwy/AlertDialogWindow.git