[WPF]動手寫一個簡單的消息對話框

来源:https://www.cnblogs.com/czwy/archive/2023/11/22/17850306.html
-Advertisement-
Play Games

今天有群友在群里問 C# 能不能在 Linux 下訪問 Access資料庫? 我覺得這很有趣,因此研究折騰了一下,也因為很久沒有寫博文了,所以特意上來寫博文分享經驗。 運行環境 操作系統:Ubuntu 22.04.3 LTS (Jammy) 開發工具:Visual Studio 2022 (17.8 ...


消息對話框是UI界面中不可或缺的組成部分,用於給用戶一些提示,警告或者詢問的視窗。在WPF中,消息對話框是系統原生(user32.dll)的MessageBox,無法通過Style或者Template來修改消息對話框的外觀。因此,當需要一個與應用程式主題風格一致的消息對話框時,只能自己動手造輪子了。

確定“輪子”的功能

消息對話框的核心功能是向用戶顯示信息,併在用戶對消息進行處理前中斷用戶的操作。根據常見的應用場景,可以梳理出以下幾點功能:

  • 支持的消息類型:提示信息、警告信息、錯誤信息、詢問信息
  • 支持的對話框類型:迷你模式(顯示簡要信息並自動關閉)、普通模式、完整模式(適用於消息內容分層級顯示)
  • 設置消息對話框是否將觸發源作為父窗體並顯示遮罩層
    主要功能如下圖所示:
    image

開始造“輪子”

消息對話框本質也是一個窗體,因此首先要做的是自定義一個彈窗的樣式,然後根據消息類型以及對話框類型定義相應的模板。

自定義視窗外觀

標準的視窗由兩個重疊的矩形組成。外部矩形是非工作區,其中包括標題欄按鈕(最小化、最大化和關閉) 、視窗邊框、調整大小和移動行為、應用程式圖標和標題以及系統菜單。它由操作系統的視窗管理器繪製和管理。其尺寸由標準操作系統設置決定。內部矩形是工作區,也就是應用程式的內容。
自定義視窗外觀主要是針對非工作區,可以通過設置屬性WindowStyleNone,或者使用 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樣式的時候,最後一行代碼僅僅是定義了一個TargetTypeview: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作為遮罩層,併在AlterDialogWindowOnClosed事件處理邏輯中刪除遮罩層。

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


您的分享是我們最大的動力!

-Advertisement-
Play Games
更多相關文章
  • 創建資料庫 (xxl-job) 導入相關表 Sql SET NAMES utf8mb4; SET FOREIGN_KEY_CHECKS = 0; -- -- Table structure for xxl_job_group -- DROP TABLE IF EXISTS `xxl_job_grou ...
  • 原文鏈接:https://openaigptguide.com/grok-ai/ Grok AI是由馬斯克推出的一款高級別的人工智慧大語言模型,旨在幫助軟體開發者以不同的口頭語言交流和表達。它是基於多種深度學習大規模固定模型架構,如ELMo、BERT和GPT,以及更多新的模式,使軟體開發者能夠快速設 ...
  • 公眾號「架構成長指南」,專註於生產實踐、雲原生、分散式系統、大數據技術分享。 在本教程中,您將通過大量示例來學習 Java 8 Stream API。 Java 在 Java 8 中提供了一個新的附加包,稱為 java.util.stream。該包由類、介面和枚舉組成,允許對元素進行函數式操作。 您 ...
  • 主要介紹了Java的Spring框架中的註解編程。第一章首先介紹了註解編程的概念,即在類或方法上添加特定的註解來完成特定功能的開發。然後解釋了為什麼要學習註解編程,主要原因是註解開發方便且與Spring框架的發展潮流相符合。接著介紹了註解的作用,包括替換XML配置和替換介面實現調用雙方的契約性。第四... ...
  • 一個提供互動式的Web UI用於生成相容MyBatisPlus框架的相關功能代碼的工具,代碼生成包括Entity、Mapper、Mapper.xml、Service、Controller等。 ...
  • 註:之前搭建的windows失敗,因為最終發現openface開源代碼中的torch_neural_net.py的某一路徑並不是windows的文件路徑,所以直接改用最簡便的docker使用。 實現需求目標: 實現兩張照片對人臉進行比對判斷是否為同一個人 openface環境搭建步驟 安裝docke ...
  • 前幾天寫了一篇文章【淺談WPF之控制項模板和數據模板】,有粉絲反饋說這兩種模板容易弄混,不知道什麼時候該用控制項模塊,什麼時候該用數據模板,以及template和itemtemplate之間的關係等,今天專門寫一篇文章,簡述WPF中各種模板及其相互關係。僅供學習分享使用,如有不足之處,還請指正。 ...
  • 1. 為什麼選擇使用泛型集合 存在的問題 ArrayList arrylist = new ArrayList() { 14, "hello", 29.7, true}; arrylist.Add("world");// object ​ double dsum = 0; foreach(var i ...
一周排行
    -Advertisement-
    Play Games
  • 移動開發(一):使用.NET MAUI開發第一個安卓APP 對於工作多年的C#程式員來說,近來想嘗試開發一款安卓APP,考慮了很久最終選擇使用.NET MAUI這個微軟官方的框架來嘗試體驗開發安卓APP,畢竟是使用Visual Studio開發工具,使用起來也比較的順手,結合微軟官方的教程進行了安卓 ...
  • 前言 QuestPDF 是一個開源 .NET 庫,用於生成 PDF 文檔。使用了C# Fluent API方式可簡化開發、減少錯誤並提高工作效率。利用它可以輕鬆生成 PDF 報告、發票、導出文件等。 項目介紹 QuestPDF 是一個革命性的開源 .NET 庫,它徹底改變了我們生成 PDF 文檔的方 ...
  • 項目地址 項目後端地址: https://github.com/ZyPLJ/ZYTteeHole 項目前端頁面地址: ZyPLJ/TreeHoleVue (github.com) https://github.com/ZyPLJ/TreeHoleVue 目前項目測試訪問地址: http://tree ...
  • 話不多說,直接開乾 一.下載 1.官方鏈接下載: https://www.microsoft.com/zh-cn/sql-server/sql-server-downloads 2.在下載目錄中找到下麵這個小的安裝包 SQL2022-SSEI-Dev.exe,運行開始下載SQL server; 二. ...
  • 前言 隨著物聯網(IoT)技術的迅猛發展,MQTT(消息隊列遙測傳輸)協議憑藉其輕量級和高效性,已成為眾多物聯網應用的首選通信標準。 MQTTnet 作為一個高性能的 .NET 開源庫,為 .NET 平臺上的 MQTT 客戶端與伺服器開發提供了強大的支持。 本文將全面介紹 MQTTnet 的核心功能 ...
  • Serilog支持多種接收器用於日誌存儲,增強器用於添加屬性,LogContext管理動態屬性,支持多種輸出格式包括純文本、JSON及ExpressionTemplate。還提供了自定義格式化選項,適用於不同需求。 ...
  • 目錄簡介獲取 HTML 文檔解析 HTML 文檔測試參考文章 簡介 動態內容網站使用 JavaScript 腳本動態檢索和渲染數據,爬取信息時需要模擬瀏覽器行為,否則獲取到的源碼基本是空的。 本文使用的爬取步驟如下: 使用 Selenium 獲取渲染後的 HTML 文檔 使用 HtmlAgility ...
  • 1.前言 什麼是熱更新 游戲或者軟體更新時,無需重新下載客戶端進行安裝,而是在應用程式啟動的情況下,在內部進行資源或者代碼更新 Unity目前常用熱更新解決方案 HybridCLR,Xlua,ILRuntime等 Unity目前常用資源管理解決方案 AssetBundles,Addressable, ...
  • 本文章主要是在C# ASP.NET Core Web API框架實現向手機發送驗證碼簡訊功能。這裡我選擇是一個互億無線簡訊驗證碼平臺,其實像阿裡雲,騰訊雲上面也可以。 首先我們先去 互億無線 https://www.ihuyi.com/api/sms.html 去註冊一個賬號 註冊完成賬號後,它會送 ...
  • 通過以下方式可以高效,並保證數據同步的可靠性 1.API設計 使用RESTful設計,確保API端點明確,並使用適當的HTTP方法(如POST用於創建,PUT用於更新)。 設計清晰的請求和響應模型,以確保客戶端能夠理解預期格式。 2.數據驗證 在伺服器端進行嚴格的數據驗證,確保接收到的數據符合預期格 ...