WPF实战:用XAML和C#打造你的第一个桌面应用(附完整代码)
WPF实战用XAML和C#打造你的第一个桌面应用附完整代码当你第一次打开Visual Studio面对WPF项目模板时可能会被那些陌生的XAML标签和C#代码后置文件搞得一头雾水。别担心这篇文章将带你从零开始通过构建一个功能完整的简易记事本应用掌握WPF开发的核心技能。我们将重点解决三个关键问题如何用XAML设计美观的界面如何用C#实现业务逻辑以及如何让两者完美协作1. 开发环境准备与项目创建在开始编码之前我们需要确保开发环境配置正确。推荐使用Visual Studio 2022社区版完全免费安装时务必勾选.NET桌面开发工作负载。如果你更喜欢轻量级体验也可以选择Visual Studio Code配合相应的C#扩展。创建新项目的步骤如下打开Visual Studio选择创建新项目搜索WPF选择WPF应用程序模板注意不是WPF类库命名项目为SimpleNotepad选择.NET 6.0或更高版本点击创建按钮完成项目初始化创建完成后你会看到以下关键文件App.xaml应用程序的入口点和全局资源定义MainWindow.xaml主窗口的界面定义MainWindow.xaml.cs主窗口的C#代码后置文件提示对于初学者建议保持解决方案资源管理器中的视图→文档大纲窗口开启它能直观展示XAML元素的层级结构。2. XAML界面设计实战我们的记事本需要以下UI元素顶部菜单栏文件、编辑、格式等工具栏常用功能快捷按钮主编辑区域多行文本框状态栏显示行号、列号等信息2.1 基础布局结构WPF提供了多种布局容器我们选择DockPanel作为根容器它特别适合创建传统的窗口布局Window x:ClassSimpleNotepad.MainWindow xmlnshttp://schemas.microsoft.com/winfx/2006/xaml/presentation xmlns:xhttp://schemas.microsoft.com/winfx/2006/xaml Title简易记事本 Height450 Width800 DockPanel !-- 菜单栏固定在顶部 -- Menu DockPanel.DockTop MenuItem Header文件 MenuItem Header新建 CommandNew/ MenuItem Header打开... CommandOpen/ MenuItem Header保存 CommandSave/ Separator/ MenuItem Header退出 ClickExit_Click/ /MenuItem MenuItem Header编辑 MenuItem Header撤销 CommandUndo/ MenuItem Header重做 CommandRedo/ Separator/ MenuItem Header剪切 CommandCut/ MenuItem Header复制 CommandCopy/ MenuItem Header粘贴 CommandPaste/ /MenuItem /Menu !-- 工具栏固定在菜单下方 -- ToolBar DockPanel.DockTop Button CommandNew ToolTip新建 Image SourceIcons/new.png Width16/ /Button Button CommandOpen ToolTip打开 Image SourceIcons/open.png Width16/ /Button Button CommandSave ToolTip保存 Image SourceIcons/save.png Width16/ /Button Separator/ Button CommandCut ToolTip剪切 Image SourceIcons/cut.png Width16/ /Button Button CommandCopy ToolTip复制 Image SourceIcons/copy.png Width16/ /Button Button CommandPaste ToolTip粘贴 Image SourceIcons/paste.png Width16/ /Button /ToolBar !-- 状态栏固定在底部 -- StatusBar DockPanel.DockBottom StatusBarItem TextBlock x:NamestatusText Text就绪/ /StatusBarItem /StatusBar !-- 主编辑区域填充剩余空间 -- TextBox x:NamemainTextBox AcceptsReturnTrue AcceptsTabTrue TextWrappingWrap VerticalScrollBarVisibilityAuto/ /DockPanel /Window2.2 样式美化与资源定义为了让应用看起来更专业我们添加一些样式定义。在Window.Resources中添加Window.Resources Style TargetTypeTextBox Setter PropertyFontFamily ValueConsolas/ Setter PropertyFontSize Value14/ Setter PropertyBackground Value#FFF5F5F5/ Setter PropertyBorderThickness Value0/ /Style Style TargetTypeStatusBar Setter PropertyBackground Value#FFE0E0E0/ Setter PropertyForeground Value#FF333333/ /Style Style TargetTypeToolBar Setter PropertyBackground Value#FFF0F0F0/ /Style /Window.Resources3. C#逻辑实现现在我们来为界面添加实际功能。打开MainWindow.xaml.cs文件开始编写业务逻辑。3.1 文件操作功能首先实现核心的文件操作功能using Microsoft.Win32; using System.IO; using System.Windows; public partial class MainWindow : Window { private string currentFilePath null; public MainWindow() { InitializeComponent(); mainTextBox.TextChanged (s, e) UpdateStatus(); } private void NewFile() { if (CheckUnsavedChanges()) { mainTextBox.Clear(); currentFilePath null; UpdateStatus(新建文件); } } private void OpenFile() { if (!CheckUnsavedChanges()) return; var openDialog new OpenFileDialog { Filter 文本文件|*.txt|所有文件|*.*, Title 打开文件 }; if (openDialog.ShowDialog() true) { try { mainTextBox.Text File.ReadAllText(openDialog.FileName); currentFilePath openDialog.FileName; UpdateStatus($已打开: {Path.GetFileName(currentFilePath)}); } catch (Exception ex) { MessageBox.Show($打开文件失败: {ex.Message}, 错误, MessageBoxButton.OK, MessageBoxImage.Error); } } } private void SaveFile(bool saveAs false) { if (currentFilePath null || saveAs) { var saveDialog new SaveFileDialog { Filter 文本文件|*.txt|所有文件|*.*, Title 保存文件 }; if (saveDialog.ShowDialog() true) { currentFilePath saveDialog.FileName; } else { return; } } try { File.WriteAllText(currentFilePath, mainTextBox.Text); UpdateStatus($已保存: {Path.GetFileName(currentFilePath)}); } catch (Exception ex) { MessageBox.Show($保存文件失败: {ex.Message}, 错误, MessageBoxButton.OK, MessageBoxImage.Error); } } private bool CheckUnsavedChanges() { if (string.IsNullOrEmpty(mainTextBox.Text)) return true; var result MessageBox.Show(当前内容未保存是否继续, 警告, MessageBoxButton.YesNoCancel, MessageBoxImage.Warning); return result switch { MessageBoxResult.Yes true, MessageBoxResult.No true, _ false }; } private void UpdateStatus(string message null) { if (!string.IsNullOrEmpty(message)) { statusText.Text message; return; } var text mainTextBox.Text; var line text.Substring(0, mainTextBox.CaretIndex).Count(c c \n) 1; var column mainTextBox.CaretIndex - text.LastIndexOf(\n, mainTextBox.CaretIndex - 1); statusText.Text currentFilePath null ? $第 {line} 行第 {column} 列 | 未保存 : $第 {line} 行第 {column} 列 | {Path.GetFileName(currentFilePath)}; } private void Exit_Click(object sender, RoutedEventArgs e) { if (CheckUnsavedChanges()) { Application.Current.Shutdown(); } } }3.2 命令绑定与快捷键WPF提供了强大的命令系统我们可以利用内置命令简化代码// 在构造函数中添加命令绑定 public MainWindow() { InitializeComponent(); // 绑定命令 CommandBindings.Add(new CommandBinding( ApplicationCommands.New, (s, e) NewFile())); CommandBindings.Add(new CommandBinding( ApplicationCommands.Open, (s, e) OpenFile())); CommandBindings.Add(new CommandBinding( ApplicationCommands.Save, (s, e) SaveFile(), (s, e) e.CanExecute !string.IsNullOrEmpty(mainTextBox.Text))); CommandBindings.Add(new CommandBinding( ApplicationCommands.Cut, (s, e) mainTextBox.Cut())); CommandBindings.Add(new CommandBinding( ApplicationCommands.Copy, (s, e) mainTextBox.Copy())); CommandBindings.Add(new CommandBinding( ApplicationCommands.Paste, (s, e) mainTextBox.Paste())); CommandBindings.Add(new CommandBinding( ApplicationCommands.Undo, (s, e) mainTextBox.Undo(), (s, e) e.CanExecute mainTextBox.CanUndo)); CommandBindings.Add(new CommandBinding( ApplicationCommands.Redo, (s, e) mainTextBox.Redo(), (s, e) e.CanExecute mainTextBox.CanRedo)); mainTextBox.TextChanged (s, e) { CommandManager.InvalidateRequerySuggested(); UpdateStatus(); }; }4. 高级功能扩展4.1 字体设置对话框让我们添加一个简单的字体设置功能!-- 在菜单中添加格式菜单项 -- MenuItem Header格式 MenuItem Header字体... ClickFontSettings_Click/ /MenuItemprivate void FontSettings_Click(object sender, RoutedEventArgs e) { var dialog new FontDialog { Owner this, FontFamily mainTextBox.FontFamily, FontSize mainTextBox.FontSize, FontWeight mainTextBox.FontWeight, FontStyle mainTextBox.FontStyle }; if (dialog.ShowDialog() true) { mainTextBox.FontFamily dialog.FontFamily; mainTextBox.FontSize dialog.FontSize; mainTextBox.FontWeight dialog.FontWeight; mainTextBox.FontStyle dialog.FontStyle; } }需要先创建FontDialog窗口!-- 添加新窗口FontDialog.xaml -- Window x:ClassSimpleNotepad.FontDialog xmlnshttp://schemas.microsoft.com/winfx/2006/xaml/presentation xmlns:xhttp://schemas.microsoft.com/winfx/2006/xaml Title字体设置 Height300 Width400 WindowStartupLocationCenterOwner Grid Margin10 Grid.RowDefinitions RowDefinition HeightAuto/ RowDefinition HeightAuto/ RowDefinition HeightAuto/ RowDefinition Height*/ RowDefinition HeightAuto/ /Grid.RowDefinitions ComboBox x:NamefontFamilyCombo Grid.Row0 DisplayMemberPathSource SelectedValuePathSource Margin0,0,0,10/ StackPanel Grid.Row1 OrientationHorizontal ComboBox x:NamefontSizeCombo Width80 Margin0,0,10,0 ComboBoxItem8/ComboBoxItem ComboBoxItem9/ComboBoxItem ComboBoxItem10/ComboBoxItem ComboBoxItem11/ComboBoxItem ComboBoxItem12/ComboBoxItem ComboBoxItem14/ComboBoxItem ComboBoxItem16/ComboBoxItem ComboBoxItem18/ComboBoxItem ComboBoxItem20/ComboBoxItem ComboBoxItem22/ComboBoxItem ComboBoxItem24/ComboBoxItem ComboBoxItem26/ComboBoxItem ComboBoxItem28/ComboBoxItem ComboBoxItem36/ComboBoxItem ComboBoxItem48/ComboBoxItem ComboBoxItem72/ComboBoxItem /ComboBox CheckBox x:NameboldCheck Content粗体 Margin0,0,10,0/ CheckBox x:NameitalicCheck Content斜体/ /StackPanel TextBlock Grid.Row2 Text示例文本 x:NamepreviewText Margin0,10,0,20 FontSize24 TextAlignmentCenter/ StackPanel Grid.Row4 OrientationHorizontal HorizontalAlignmentRight Button Content确定 Width80 Margin0,0,10,0 ClickOK_Click/ Button Content取消 Width80 ClickCancel_Click/ /StackPanel /Grid /Window// FontDialog.xaml.cs public partial class FontDialog : Window { public FontFamily FontFamily { get; set; } public double FontSize { get; set; } public FontWeight FontWeight { get; set; } public FontStyle FontStyle { get; set; } public FontDialog() { InitializeComponent(); Loaded (s, e) { fontFamilyCombo.ItemsSource Fonts.SystemFontFamilies .OrderBy(f f.Source); fontSizeCombo.Text FontSize.ToString(); boldCheck.IsChecked FontWeight FontWeights.Bold; italicCheck.IsChecked FontStyle FontStyles.Italic; UpdatePreview(); }; } private void UpdatePreview() { previewText.FontFamily FontFamily; previewText.FontSize FontSize; previewText.FontWeight FontWeight; previewText.FontStyle FontStyle; } private void OK_Click(object sender, RoutedEventArgs e) { FontFamily new FontFamily(fontFamilyCombo.SelectedValue.ToString()); FontSize double.Parse(fontSizeCombo.Text); FontWeight boldCheck.IsChecked true ? FontWeights.Bold : FontWeights.Normal; FontStyle italicCheck.IsChecked true ? FontStyles.Italic : FontStyles.Normal; DialogResult true; } private void Cancel_Click(object sender, RoutedEventArgs e) { DialogResult false; } }4.2 最近打开文件列表增强用户体验添加最近打开文件功能// 在MainWindow类中添加 private const int MaxRecentFiles 5; private ObservableCollectionstring recentFiles new ObservableCollectionstring(); // 修改构造函数 public MainWindow() { InitializeComponent(); LoadRecentFiles(); // ...其他初始化代码 } private void LoadRecentFiles() { if (Properties.Settings.Default.RecentFiles ! null) { foreach (var file in Properties.Settings.Default.RecentFiles) { if (File.Exists(file)) { recentFiles.Add(file); } } } // 动态生成最近文件菜单项 var recentMenu new MenuItem { Header 最近文件 }; recentMenu.ItemsSource recentFiles.Select(file { var item new MenuItem { Header Path.GetFileName(file), Tag file }; item.Click (s, e) OpenRecentFile(file); return item; }); // 插入到文件菜单中 var fileMenu (MenuItem)Menu.Items[0]; fileMenu.Items.Insert(fileMenu.Items.Count - 1, recentMenu); } private void OpenRecentFile(string path) { if (!CheckUnsavedChanges()) return; try { mainTextBox.Text File.ReadAllText(path); currentFilePath path; UpdateStatus($已打开: {Path.GetFileName(path)}); UpdateRecentFiles(path); } catch (Exception ex) { MessageBox.Show($打开文件失败: {ex.Message}, 错误, MessageBoxButton.OK, MessageBoxImage.Error); } } private void UpdateRecentFiles(string path) { // 如果文件已经在列表中先移除 if (recentFiles.Contains(path)) { recentFiles.Remove(path); } // 添加到列表顶部 recentFiles.Insert(0, path); // 限制最大数量 while (recentFiles.Count MaxRecentFiles) { recentFiles.RemoveAt(recentFiles.Count - 1); } // 保存到设置 Properties.Settings.Default.RecentFiles recentFiles.ToList(); Properties.Settings.Default.Save(); }5. 应用打包与部署完成开发后我们需要将应用打包以便分发。WPF应用可以通过ClickOnce或MSI安装包部署ClickOnce部署简单快捷在解决方案资源管理器中右键项目选择发布按照向导设置发布位置可以是文件夹、FTP或网络共享配置安装模式和更新设置点击发布生成安装程序MSI安装包更专业在解决方案中添加新项目搜索并选择安装项目模板添加主输出Primary Output和任何依赖项配置快捷方式、注册表项等生成项目获得.msi安装文件提示对于.NET Core/5的WPF应用还可以考虑使用dotnet publish命令生成独立部署包这样用户无需安装.NET运行时。