序号th> | 书名th> | 作者th> | 价格th> tr> thead> |
---|---|---|---|
{{i+1}}th> | {{item.Title}}td> | {{item.Author}}td> | {{item.Price}}td> tr> tbody> table> div> div> body> |
-
2022-02-26 01:17:10
Python微信订餐小程序课程视频
https://edu.csdn.net/course/detail/36074
Python实战量化交易理财系统
https://edu.csdn.net/course/detail/35475
使用 WebView2 封装一个生成 PDF 的 WPF 控件
最近在迁移项目到 .net6,发现项目中用的 PDF 库不支持 .net6,于是想着换一个库。结果找了一大圈,发现不是版本不支持,就是收费。
嗐!还能咋办,只能自己搞一个 PDF 生成控件咯。环境准备 WPF + WebView2 + Vue
WebView2
- WebView2.CoreWebView2.PrintToPdfAsync 可以将 html 文件生成 pdf。
- CEF 也有类似的 API,Evergreen WebView2 会自动更新,而且不需要将库打包到程序中,所以就用它了。
- WebView2 需要先安装到本机,下载链接。
Vue
- 直接操作 Dom 不够方便,Vue 用法跟 WPF 的绑定方式又很相似,使用 vue 来定义 pdf 的 Html 的模板,可以让不会 h5 的同事也能轻松写模板文件,所以这里用 Vue 来操作 Dom 和数据绑定。
Prism
- WPF 项目常用的框架,我这里用来注册预览 PDF 的弹窗,以及给弹窗传参。
以打印一个表格为例
1. 定义要生成 PDF 的表格
复制代码
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
- 26
- 27
- 28
- 29
- 30
- 31
- 32
- 33
- 34
- 35
- 36
- 37
- 38
- 39
- 40
- 41
- 42
- 43
- 44
- 45
- 46
- 47
- 48
- 49
- 50
- 51
- 52
- 53
- 54
- 55
c#`// BuyBookView.xaml
“1”
Margin=“24,0”
AutoGenerateColumns=“False”
FontSize=“16”
IsReadOnly=“True”
ItemsSource="{Binding Books}"
TextBlock.TextAlignment=“Center”>“*”
Binding="{Binding Title}"
Header=“书名”
HeaderStyle="{StaticResource CenterGridHeaderStyle}" />
“100”
Binding="{Binding Author}"
Header=“作者”
HeaderStyle="{StaticResource CenterGridHeaderStyle}" />
“100”
Binding="{Binding Price}"
Header=“价格”
HeaderStyle="{StaticResource CenterGridHeaderStyle}" />// BuyBookViewModel
public BuyBookViewModel(IDialogService dialogService)
{
Title = “鸭霸的购书目录”;
Books = new List
{
new()
{
Title = “JavaScript权威指南 原书第7版”,
Author = “巨佬1”,
Price = 90.3
},
new()
{
Title = “深入浅出node.js”,
Author = “巨佬2”,
Price = 57.8
},
new()
{
Title = “编码:隐匿在计算机软硬件背后的语言”,
Author = “巨佬3”,
Price = 89.00
}
};
}`2. 定义预览 PDF 的弹窗
- 在 xaml 中引入 WebView2
复制代码
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
c#`// PrintPdfView.xml
…
xmlns:wpf=“clr-namespace:Microsoft.Web.WebView2.Wpf;assembly=Microsoft.Web.WebView2.Wpf”
…“24”>
“Auto” />
“webView2” />
“1”>
“save”
HorizontalAlignment=“Right”
Content=“保存” />`- 在 viewmodel 中定义弹窗接收的参数以及弹窗的属性
复制代码
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
- 26
- 27
- 28
- 29
- 30
- 31
- 32
- 33
c#`// PrintPdfViewModel.cs
public class PrintPdfViewModel : BindableBase, IDialogAware
{
private string _template;
///
/// PDF 的 html 模板
///
public string Template
{
get => _template;
set => SetProperty(ref _template, value);
}private ExpandoObject _data;
///
/// 传递给 pdf 的数据
///
public ExpandoObject Data
{
get => _data;
set => SetProperty(ref _data, value);
}public void OnDialogOpened(IDialogParameters parameters)
{
// 弹窗接收 template 和 data 两个参数
parameters.TryGetValue(“template”, out _template);
parameters.TryGetValue(“data”, out _data);
}public string Title => “预览 PDF”;
}`3. 定义 WebView2 生成 PDF 的逻辑和 pdf 的模板文件
- 使用 vue 来定义 pdf 模板的逻辑,和调用 WebView2.CoreWebView2.PrintToPdfAsync 来生成 PDF。
- 因为客户端经常运行在内网或无网环境,所以这里就不用 cdn 引入 vuejs,而是直接将 vuejs 嵌入到客户端的资源文件中。
- 调用 WebView2.CoreWebView2.PostWebMessageAsJson 从 WPF 向 WebView2 发送数据。
复制代码
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
- 26
- 27
- 28
- 29
- 30
- 31
c#`// PrintPdfViewModel.xaml.cs
///
/// 配置 WebView2,加载 vuejs,加载 pdf 模板,传递数据到 html 中
///
///
private async Task Load()
{
await webView2.EnsureCoreWebView2Async();
webView2.CoreWebView2.Settings.AreDefaultContextMenusEnabled = false; // 禁止右键菜单var assembly = Assembly.GetExecutingAssembly();
var resourceName = “PrintPdf.Views.vue.global.js”;using var stream = assembly.GetManifestResourceStream(resourceName);
if (stream != null)
{
using var reader = new StreamReader(stream);
var vue = await reader.ReadToEndAsync();
await webView2.CoreWebView2.AddScriptToExecuteOnDocumentCreatedAsync(vue); // 加载 vuejs
}var vm = (PrintPdfViewModel)DataContext;
webView2.CoreWebView2.NavigateToString(vm.Template); // 加载 pdf 模板
webView2.CoreWebView2.NavigationCompleted += (sender, args) =>
{
var json = JsonSerializer.Serialize(vm.Data);
webView2.CoreWebView2.PostWebMessageAsJson(json); // 将数据传递到 html 中
};
}`- 点击保存时,选择路径并生成 PDF 文件。
复制代码
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
c#`// PrintPdfViewModel.xaml.cs
save.Click += async (sender, args) =>
{
var saveFileDialog = new SaveFileDialog
{
Filter = “txt files (.pdf)|.pdf”,
RestoreDirectory = true,
InitialDirectory = Environment.GetFolderPath(Environment.SpecialFolder.MyDocuments),
FileName = $“test.pdf”
};
var result = saveFileDialog.ShowDialog();if (result != true)
return;var printSetting = webView2.CoreWebView2.Environment.CreatePrintSettings();
printSetting.ShouldPrintBackgrounds = true;var saveResult = await webView2.CoreWebView2.PrintToPdfAsync($"{saveFileDialog.FileName}", printSetting);
};`- 定义 pdf 的打印模板,并且使用 Vue 来实现绑定功能,调用 webview.addEventListener 来监听 WPF 传递给 WebView2 的数据。
复制代码
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
- 26
- 27
- 28
- 29
- 30
- 31
- 32
- 33
- 34
- 35
- 36
- 37
- 38
- 39
- 40
- 41
- 42
- 43
- 44
- 45
- 46
- 47
- 48
- 49
- 50
html`
... head>{{title}} h3> div>
复制代码
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
c#`// BuyBookView.xaml
“{Binding ShowPrintViewCommand}” Content="预览 PDF1 " />// BuyBookViewModel
ShowPrintViewCommand = new DelegateCommand(() =>
{
var assembly = Assembly.GetExecutingAssembly();
var resourceName = $“PrintPdf.ViewModels.test_print.html”;using var stream = assembly.GetManifestResourceStream(resourceName); // 加载模板
if (stream == null) return;
using var reader = new StreamReader(stream);
var t = reader.ReadToEnd();
dynamic d = new ExpandoObject(); // 转换数据
d.title = Title;
d.books = Books;var p = new DialogParameters
{
{“template”, t},
{“data”, d}
};
dialogService.ShowDialog(nameof(PrintPdfView), p, null);
});`4. 效果
5. 优化一下
现在功能已经差不多了,但是 html 模板需要写的 js 太多,而且这是一个 WPF 控件,所以应该封装一下,最好用起来跟 wpf 一样才更好。
既然都用 vue 了,那就用 vue 封装一下组件。- vue 封装一下表格控件,并且暴露出属性 itemSource 和 columns
复制代码
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
- 26
- 27
- 28
- 29
- 30
javascript
// controls.js const DataGrid = { props: ["itemsSource", "columns"], template:
|
{{column.Header}}{{item[column.Binding]}}
|} const DocumentHeader = { props: ["title"], template:
{{title}}
};
- 将 controls.js 注入到 WebView2 中
复制代码
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
c#`var assembly = Assembly.GetExecutingAssembly();
var controlsFile = “PrintPdf.Views.controls.js”;using var controlsStream = assembly.GetManifestResourceStream(controlsFile);
using var controlsReader = new StreamReader(controlsStream);
var controls = await controlsReader.ReadToEndAsync();
await webView2.CoreWebView2.AddScriptToExecuteOnDocumentCreatedAsync(controls);`- 现在 html 模板中的 data-grid 组件就跟 WPF 的 DataGrid 控件很相似了
复制代码
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
- 26
- 27
- 28
- 29
- 30
- 31
- 32
- 33
- 34
- 35
html`
... head>document-header> data-grid> div> body>function generate(data) {
Vue.createApp({
data() {
return {
title,columns,books
} = data;},
components: {
DataGrid,
DocumentHeader
}
}).mount(’#app’);
}script>
html>`最后
觉得对你有帮助点个推荐或者留言交流一下呗!
源码 https://github.com/yijidao/blog/tree/master/WPF/PrintPdf
更多相关内容 -
怎样封装WPF用户控件的一些实践
2020-09-08 13:47:10JHRS开发框架之公用组件WPF用户控件封装,这个系列的文章旨在记录工作中使用WPF开发新的医疗项目中,有感于必须统一掉一些规范上的事情,并且提高团队开发效率,遂折腾了这么一个半吊子的框架,这个标题WPF企业级...JHRS开发框架之公用组件WPF用户控件封装,这个系列的文章旨在记录工作中使用WPF开发新的医疗项目中,有感于必须统一掉一些规范上的事情,并且提高团队开发效率,遂折腾了这么一个半吊子的框架,这个标题WPF企业级开发框架搭建指南,2020从入门到放弃可能会唬住一些人,但看到这些零碎文字的朋友就凑和着看吧,如果能帮助到你,那也荣幸了。
继上一篇介绍了怎样封装ViewModel的基类,但随着项目大了,一个功能点一个功能点的做,真的累,很多系统里面,在局部有很多相似的功能,数据展示几乎一样的,或许不一样的只是摆放的位置,显示的样式不同罢了;这种东西,一个功能一个功能的实现,那就有点朝着996的状态发展了;因此在JHRS框架中,也体现了懒人干活的思想,那就是能封装成控件的,坚决搞成一个控件,供大家享乐。
JHRS开发框架之公用组件WPF用户控件封装
当然,用户控件可以套在用户控件里面,一个拥有复杂功能的页面,可以由众多的用户控件组成,最终你会发现,用户控件封装得越优雅,做复杂的功能页面也不会很难了,只需要像搭积木那样,把控件丢上去,数据绑定上就完事了,最后如果要调整样式,稍微调整下就OK了。
WPF用户控件封装
因为框架中引入了Prism将各子系统模块化了,所以我们在WPF用户控件封装的时候,分为两种情况,一种是封装整个系统公用的用户控件,另外一种是各子模块自己的用户控件;整个系统公用的用户控件,需要保持着高扩展性,灵活性,即使后期对该控件增加功能,也要尽量做到不影响已经使用该控件的页面(Page),还需要让使用的页面可以灵活的设置一些属性以满足不同页面(Page)的功能需求。
在框架中只封装了3个基本的用户控件,动态列的DataGrid,可调接口的Combobox,动态分页表格。
JHRS开发框架动态列的DataGrid
大部分管理系统,都离不开表格展示数据,而WPF项目中,基本上都会使用DataGrid来展示数据;熟悉WPF开发的朋友都知道,如果手工撸一个表格,那代码贼烦人,需要一个列一个列的写代码并绑定数据;而在JHRS框架中提供的思路是基于注解的方式(自定义BindDescriptionAttribute类用于描述每列)动态生成每一列数据并自动绑定数据,对于复杂的列,如某一列里面展示为下拉框(ComoboBox)或者更复杂的展示,只需要在资源(Resources)里面定义DataTemplate即可,然后动态加载就可以了。对于最每一行的操作列,也是一样的套路。
动态列的DataGrid封装思路是:编写一个DataGridEx类,继承自DataGrid类,在DataGridEx类中,需要定义一个依赖属性我们称为DataSource,用它来绑定数据,然后在DataSourceProperty的回调函数里面把DataSource赋值给原本的 ItemSource属性,最后重写OnInitialized方法,将数据源传入DataGrid的扩展方法GenerateColumns动态生成列就完成了WPF用户控件封装,详见下方代码。
DataGridEx类源码
/// <summary> /// 輕量級的DataGrid擴展 /// </summary> public class DataGridEx : DataGrid { /// <summary> /// 構造函數 /// </summary> public DataGridEx() { this.AutoGenerateColumns = false; this.Loaded += DataGridEx_Loaded; this.LoadingRow += PagingDataList_LoadingRow; } /// <summary> /// 给表格添加样式 /// </summary> /// <param name="sender"></param> /// <param name="e"></param> private void DataGridEx_Loaded(object sender, RoutedEventArgs e) { this.CanUserAddRows = false; } /// <summary> /// 生成序号 /// </summary> /// <param name="sender"></param> /// <param name="e"></param> private void PagingDataList_LoadingRow(object sender, DataGridRowEventArgs e) { if (EnableRowNumber) //需要分页 e.Row.Header = e.Row.GetIndex() + 1; } /// <summary> /// 操作列key /// </summary> public string OperatingKey { get; set; } = string.Empty; /// <summary> /// 操作列的宽度 /// </summary> public DataGridLength OperationWidth { get; set; } /// <summary> /// 是否启用序号 /// </summary> public bool EnableRowNumber { get; set; } = true; /// <summary> /// 禁止显示的列 /// </summary> public string DisableCloumn { get; set; } public IEnumerable<object> DataSource { get { return (IEnumerable<object>)GetValue(DataSourceProperty); } set { SetValue(DataSourceProperty, value); } } private bool IsGenerateColumns = false; // Using a DependencyProperty as the backing store for DataSource. This enables animation, styling, binding, etc... public static readonly DependencyProperty DataSourceProperty = DependencyProperty.Register("DataSource", typeof(IEnumerable<object>), typeof(DataGridEx), new PropertyMetadata((d, e) => { DataGridEx u = d as DataGridEx; u.ItemsSource = u.DataSource; if (u.IsGenerateColumns || u.DataSource == null || u.DataSource.Count() == 0) return; var index = 0; if (u.EnableRowNumber) { var acolumn = new DataGridTextColumn { Header = "序号", Width = new DataGridLength(50), Binding = new Binding("Header") { RelativeSource = new RelativeSource(RelativeSourceMode.FindAncestor, typeof(DataGridRow), 1) } }; u.Columns.Insert(0, acolumn); index++; } u.GenerateColumns(index, u.ItemsSource, u.OperatingKey, u.OperationWidth); u.IsGenerateColumns = true; })); //protected override void OnInitialized(EventArgs e) //{ // if (IsGenerateColumns || ItemsSource == null) return; // var index = 0; // if (EnableRowNumber) // { // var acolumn = new DataGridTextColumn // { // Header = "序号", // Width = new DataGridLength(50), // Binding = new Binding("Header") { RelativeSource = new RelativeSource(RelativeSourceMode.FindAncestor, typeof(DataGridRow), 1) } // }; // this.Columns.Insert(0, acolumn); // index++; // } // this.GenerateColumns(index, ItemsSource, OperatingKey, OperationWidth); // IsGenerateColumns = true; //} }
以上的基本上是完整的源码,github是参见这里。
DataGridExtensions扩展类源码
/// <summary> /// DataGrid扩展方法 /// </summary> public static class DataGridExtensions { /// <summary> /// 动态生成列 /// </summary> /// <param name="dataGrid">DataGrid控件实例</param> /// <param name="index">列插入位置</param> /// <param name="data">数据源</param> /// <param name="operationKey">操作列资源</param> /// <param name="operationWidth">操作列宽度</param> public static void GenerateColumns(this DataGrid dataGrid, int index, object data, string operationKey, DataGridLength operationWidth) { IList<BindDescriptionAttribute> list = GetColumns(data); //Window win = Application.Current.Windows.OfType<Window>().SingleOrDefault(x => x.IsActive); //Page page = win.GetChildObject<Page>("page"); //if (page == null) throw new Exception("未獲取到當前窗口名稱爲page的(Page)頁面對象,原因:沒有爲Page設置Name,且名稱必須爲【page】!"); Page page = GetParentObject<Page>(dataGrid, "page"); for (int i = 0; i < list.Count; i++) { switch (list[i].ShowAs) { case ShowScheme.普通文本: dataGrid.Columns.Insert(i + index, new DataGridTextColumn { Header = list[i].HeaderName, Binding = new Binding(list[i].PropertyName), Width = list[i].Width }); break; case ShowScheme.自定义: if (page.FindResource(list[i].ResourceKey) != null) { DataGridTemplateColumn val = new DataGridTemplateColumn(); val.Header = list[i].HeaderName; val.Width = list[i].Width; val.CellTemplate = page.FindResource(list[i].ResourceKey) as DataTemplate; dataGrid.Columns.Insert(i + index, val); } break; } } if (!string.IsNullOrWhiteSpace(operationKey) && page != null) { var resource = page.FindResource(operationKey); if (resource!=null) { var col = new DataGridTemplateColumn() { Header = "操作", Width = operationWidth }; col.CellTemplate = resource as DataTemplate; dataGrid.Columns.Add(col); } } } /// <summary> /// 获取数据源对象到列的映射关系 /// </summary> /// <param name="data"></param> /// <returns></returns> private static IList<BindDescriptionAttribute> GetColumns(object data) { List<BindDescriptionAttribute> list = new List<BindDescriptionAttribute>(); var pros = data.GetType().GenericTypeArguments[0].GetProperties(); foreach (var item in pros) { var a = item.GetCustomAttribute<BindDescriptionAttribute>(); if (a != null) { a.PropertyName = item.Name; list.Add(a); } } return list.OrderBy(x => x.DisplayIndex).ToArray(); } /// <summary> /// 查找父级控件 /// </summary> /// <typeparam name="T"></typeparam> /// <param name="obj"></param> /// <param name="name"></param> /// <returns></returns> public static T GetParentObject<T>(DependencyObject obj, string name) where T : FrameworkElement { DependencyObject parent = VisualTreeHelper.GetParent(obj); while (parent != null) { if (parent is T && (((T)parent).Name == name | string.IsNullOrEmpty(name))) { return (T)parent; } parent = VisualTreeHelper.GetParent(parent); } return null; } }
完整的参见这里。
BindDescriptionAttribute注解类
在调用web api从服务器端返回的数据中,需要在本地的WPF项目定义相关的实体类,并且在实体类的属性上标记绑定描述类(BindDescriptionAttribute),这个类直接描述了展面如何展示(一般用DataGrid,可按此思路扩展不规则表单),这也是WPF用户控件封装之前的准备工作。
/// <summary> /// DataGrid绑定数据源描述 /// </summary> public class BindDescriptionAttribute : Attribute { /// <summary> /// 列名 /// </summary> public string HeaderName { get; set; } /// <summary> /// 显示为 /// </summary> public ShowScheme ShowAs { get; set; } /// <summary> /// 显示顺序 /// </summary> public int DisplayIndex { get; set; } /// <summary> /// DataGrid列绑定属性名称 /// </summary> public string PropertyName { get; set; } /// <summary> /// 应用内的容模板Key /// </summary> public string ResourceKey { get; set; } /// <summary> /// 列宽 /// </summary> public DataGridLength Width { get; set; } /// <summary> /// 列宽ByGrid /// </summary> public GridLength CloumnWidth { get; set; } /// <summary> /// DataGrid绑定数据源描述 /// </summary> /// <param name="headerName">列名</param> /// <param name="showAs">显示为</param> /// <param name="width">宽度</param> /// <param name="displayIndex">显示顺序</param> /// <param name="resourceKey">自定义列Key</param> public BindDescriptionAttribute(string headerName, ShowScheme showAs = ShowScheme.普通文本, string width = "Auto", int displayIndex = 0, string resourceKey = "") { HeaderName = headerName; DisplayIndex = displayIndex; ResourceKey = resourceKey; ShowAs = showAs; var convert = new DataGridLengthConverter(); Width = (DataGridLength)convert.ConvertFrom(width); var gridCOnvert = new GridLengthConverter(); CloumnWidth = (GridLength)gridCOnvert.ConvertFrom(width); if (showAs == ShowScheme.自定义 && string.IsNullOrWhiteSpace(resourceKey)) throw new ArgumentException($"自定义列时需要指定{nameof(resourceKey)}参数!"); } } /// <summary> /// 展示方式 /// </summary> public enum ShowScheme { 普通文本 = 1, 自定义 = 4 }
上方的枚举ShowScheme则描述了对应的列的显示方案,是该使用普通的文本还是加载数据模板(DataTemplate)。
如何使用DataGridEx实现动态表格功能
要使用自己扩展的动态DataGrid其实是跟使用常规的DataGrid是一样的,只是区别是绑定数据是使用DataSource属性而已,下面就是WPF用户控件封装之动态表格xaml,如下代码所示:
<Page x:Class="JHRS.RegisterManagement.Views.RegisterList" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:prism="http://prismlibrary.com/" xmlns:d="http://schemas.microsoft.com/expression/blend/2008" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" xmlns:c="clr-namespace:JHRS.Core.Controls.Common;assembly=JHRS.Core" xmlns:b="http://schemas.microsoft.com/xaml/behaviors" prism:ViewModelLocator.AutoWireViewModel="True" mc:Ignorable="d" d:DesignHeight="450" d:DesignWidth="800" x:Name="page" Title="RegisterList" Background="White"> <b:Interaction.Triggers> <b:EventTrigger EventName="Loaded" > <b:InvokeCommandAction Command="{Binding LoadedCommand}" CommandParameter="{Binding ElementName=page}" /> </b:EventTrigger> </b:Interaction.Triggers> <Grid x:Name="maskContainer"> <c:DataGridEx DataSource="{Binding PageData}" IsReadOnly="True"/> </Grid> </Page>
上面代中<c:DataGridEx DataSource=”{Binding PageData}” IsReadOnly=”True”/>这行就是WPF用户控件封装的动态表格绑定数据的用法,而PageData则是来自ViewModel定义的一个IEnumerable<object>属性,只需要在当前页面(Page)的ViewModel里面调用接口获取数据给PageData赋值就可以了,如下代码所示:
/// <summary> /// 綁定分頁數據 /// </summary> [WaitComplete] protected async override Task<object> BindPagingData() { List<Account> list = new List<Account>(); for (int i = 0; i < 15; i++) { list.Add(new Account { Name = "趙佳仁" + i, RegTime = DateTime.Now.AddDays(i), RoleName = "管理員" + i, Title = "無職" + i, UserID = 100 + i }); } PageData = list; await Task.Delay(200); return true; }
而Account类则是这样定义的。
public class Account { [BindDescription("用戶ID")] public int UserID { get; set; } [BindDescription("用戶名")] public string Name { get; set; } [BindDescription("註冊時間")] public DateTime RegTime { get; set; } [BindDescription("角色名穩")] public string RoleName { get; set; } [BindDescription("職級")] public string Title { get; set; } }
完整的代码参见这里。下图是最终的效果:
WPF用户控件封装
Combobox扩展控件
原生的Combobox是WPF提供的下拉框控件,如果约定在整个系统里面,所有的下拉框控件默认项为请选择,如下图所示:
统一的给加上这个的话,在框架中是这样做的,先自定义一个BaseComboBox类,继承自ComboBox类,并在代码里面添加一个默认项,代码如下:
using JHRS.Http; using System; using System.Collections; using System.Collections.Generic; using System.Linq; using System.Net.Http; using System.Text; using System.Threading.Tasks; using System.Windows; using System.Windows.Controls; namespace JHRS.Core.Controls.Common { /// <summary> /// 下拉框控件基类 /// </summary> public abstract class BaseComboBox : ComboBox { /// <summary> /// 下拉框数据源 /// </summary> public IList Data { get { return (IList)GetValue(DataProperty); } set { AddDefault(value); SetValue(DataProperty, value); } } /// <summary> /// 已登录获取到Token客户端对象 /// </summary> protected HttpClient AuthClient => AuthHttpClient.Instance; /// <summary> /// 服务器配置 /// </summary> protected string BaseUrl => AuthHttpClient.Instance.BaseAddress!.ToString(); // Using a DependencyProperty as the backing store for Data. This enables animation, styling, binding, etc... public static readonly DependencyProperty DataProperty = DependencyProperty.Register("Data", typeof(IList), typeof(BaseComboBox), new PropertyMetadata(null, (d, e) => { BaseComboBox c = (BaseComboBox)d; var list = e.NewValue as IList; if (list != null) c.AddDefault(list); c.Data = list; c.ItemsSource = list; })); /// <summary> /// 构造函数 /// </summary> public BaseComboBox() { this.Initialized += OnInitialized; } /// <summary> /// 下拉框初始化事件,子类实现,可以加载各自数据。 /// </summary> /// <param name="sender"></param> /// <param name="e"></param> protected abstract void OnInitialized(object sender, EventArgs e); private string DefaultSelectedValue = "-1"; private string DefaultSelectedText = "—請選擇—"; /// <summary> /// 添加默认项:请选择 /// </summary> /// <param name="data"></param> private void AddDefault(IList data) { if (data == null || data.Count == 0) return; var pros = data[0].GetType().GetProperties(); bool hasSelect = false; var s = pros.FirstOrDefault(x => x.Name == SelectedValuePath); var d = pros.FirstOrDefault(x => x.Name == DisplayMemberPath); if (s == null) throw new Exception("未給ComboBox指定SelectedValuePath屬性,注意:屬性區分大小寫!"); if (d == null) throw new Exception("未给ComboBox指定DisplayMemberPath屬性,注意:屬性區分大小寫!"); foreach (var item in data) { if (s == d && (s.GetValue(item, null) + "") == DefaultSelectedText) { hasSelect = true; break; } else if ((s.GetValue(item, null) + "") == DefaultSelectedValue && (d.GetValue(item, null) + "") == DefaultSelectedText) { hasSelect = true; break; } } if (hasSelect == false) { var subType = data.GetType().GenericTypeArguments[0]; if (subType.Name.StartsWith("<>f__AnonymousType")) return; var m = Activator.CreateInstance(subType); if (s != d) { s.SetValue(m, Convert.ChangeType(DefaultSelectedValue, s.PropertyType), null); d.SetValue(m, Convert.ChangeType(DefaultSelectedText, d.PropertyType), null); } else { d.SetValue(m, Convert.ChangeType(DefaultSelectedText, d.PropertyType), null); } data.Insert(0, m); } } } }
上面的就是完整代码,需要注意的是,在上面的代码中,处理匿名类型有Bug,并没有修复,强类型的绑定是会添加【—請選擇—】这个默认项的。
业务相关的ComboBox下拉框
在实际项目里面,会有很多下拉框的数据源是需要调用接口获取的,将它封装后就可以避免在很多的功能页面(Page)或者控件里面再调接口来获取数据给ComboBox绑定数据,因此封装后直接拖过去用就完事了,这是WPF用户控件封装之下拉框。
在框架里面封装了像科室,字典,通用业务状态的下拉框,这里只放一个科室的下拉框示例代码,因为科室是需要调接口获取的,下方注释掉的WPF用户控件封装代码就是真实项目中调用接口获取数据来绑定的代码。
using JHRS.Core.Controls.Common; using JHRS.Core.Models; using System; using System.Collections.Generic; namespace JHRS.Core.Controls.DropDown { /// <summary> /// 科室下拉框 /// </summary> public class DepartmentComboBox : BaseComboBox { /// <summary> /// 初始化科室數據,可調用接口獲取數據。 /// </summary> /// <param name="sender"></param> /// <param name="e"></param> protected override void OnInitialized(object sender, EventArgs e) { this.DisplayMemberPath = "Name"; this.SelectedValuePath = "Id"; //var response = await RestService.For<IDepartmentApi>(AuthHttpClient.Instance).GetAll(); //if (response.Succeeded) //{ // Data = response.Data as IList; //} List<DepartmentOutputDto> list = new List<DepartmentOutputDto>(); for (int i = 0; i < 20; i++) { list.Add(new DepartmentOutputDto { Id = i + 1, Name = $"測試科室{i + 1}" }); } base.Data = list; } } }
封装后下拉框怎样使用
如下图所示一样,在演示框架中,WPF用户控件封装之后的控件是直接将其拖到用户控件相关位置,然后绑定你需要获取的值即可,使用的地方不需要关注科室的数据从哪儿来的,你只需要知道你用什么属性取接收选中的值即可。
以上就是使用的方法,接下来看看如何封装分页表格。
动态分页表格
每个系统里面分页表格是大头,或者说是比较复杂的功能,而在框架里面是将表格和分页控件封装到一起形成一个用户控件,在需要使用的地方,也是直接拖过去,并调用分页接口获取数据绑定即可;WPF用户控件封装表格的每一列展示什么数据,也是采用最上面介绍的最基础的动态表格思想解决的。
分页表格控件XAML代码
<UserControl x:Class="JHRS.Core.Controls.DataGrids.PagingDataGrid" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:b="http://schemas.microsoft.com/xaml/behaviors" xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" xmlns:d="http://schemas.microsoft.com/expression/blend/2008" xmlns:hc="https://handyorg.github.io/handycontrol" xmlns:local="clr-namespace:JHRS.Core.Controls.DataGrids" Loaded="UserControl_Loaded"> <Grid Name="ucDataGrid"> <Grid.RowDefinitions> <RowDefinition/> <RowDefinition Height="5"/> <RowDefinition Height="30"/> </Grid.RowDefinitions> <DataGrid x:Name="pagingDataList" Grid.Row="0" IsReadOnly="True" ItemsSource="{Binding PageData}" CanUserAddRows="False" SelectionMode="Single" LoadingRow="pagingDataList_LoadingRow"> <DataGrid.Columns> <DataGridTextColumn Binding="{Binding Header, RelativeSource={RelativeSource AncestorType={x:Type DataGridRow}, Mode=FindAncestor}}" CanUserSort="False" Header="序号" IsReadOnly="True" Width="40"/> <DataGridTemplateColumn Width="40" x:Name="isCheckbox"> <DataGridTemplateColumn.Header> <CheckBox Click="CheckBox_Click"></CheckBox> </DataGridTemplateColumn.Header> <DataGridTemplateColumn.CellTemplate> <DataTemplate> <CheckBox Click="chkItem_Click" x:Name="chkItem" VerticalAlignment="Center" HorizontalAlignment="Center"></CheckBox> </DataTemplate> </DataGridTemplateColumn.CellTemplate> </DataGridTemplateColumn> </DataGrid.Columns> </DataGrid> <hc:Pagination x:Name="ucPagination" Grid.Row="2" HorizontalAlignment="Right" MaxPageCount="{Binding PagingData.MaxPageCount}" PageIndex="{Binding PagingData.PageIndex, Mode=TwoWay}"> <b:Interaction.Triggers> <b:EventTrigger EventName="PageUpdated" > <b:InvokeCommandAction Command="{Binding ChangePageIndexCommand}"/> </b:EventTrigger> </b:Interaction.Triggers> </hc:Pagination> </Grid> </UserControl>
在这个WPF用户控件封装的xaml代码中,主要放了两个控件,一个DataGrid表格控件,一个Pagination分页控件。后台的C#代码如下:
using JHRS.Core.Extensions; using JHRS.Filter; 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.Data; using System.Windows.Documents; using System.Windows.Input; using System.Windows.Media; using System.Windows.Media.Imaging; using System.Windows.Navigation; using System.Windows.Shapes; namespace JHRS.Core.Controls.DataGrids { /// <summary> /// PagingDataGrid.xaml 的交互逻辑 /// </summary> public partial class PagingDataGrid : UserControl { public PagingDataGrid() { InitializeComponent(); pagingDataList.AutoGenerateColumns = false; } /// <summary> /// 生成序号 /// </summary> /// <param name="sender"></param> /// <param name="e"></param> private void pagingDataList_LoadingRow(object sender, DataGridRowEventArgs e) { if (EnablePagination) //需要分页 e.Row.Header = (PagingData.PageIndex - 1) * PagingData.PageSize + e.Row.GetIndex() + 1; else //不需要分页 e.Row.Header = e.Row.GetIndex() + 1; } /// <summary> /// 表格数据源 /// </summary> public IEnumerable<object> PageData { get { return (IEnumerable<object>)GetValue(PageDataProperty); } set { SetValue(PageDataProperty, value); } } //是否已经生成了列并建立了绑定关系 private bool IsGenerateColumns = false; // Using a DependencyProperty as the backing store for PageData. This enables animation, styling, binding, etc... public static readonly DependencyProperty PageDataProperty = DependencyProperty.Register("PageData", typeof(IEnumerable<object>), typeof(PagingDataGrid), new PropertyMetadata((d, e) => { PagingDataGrid pagingDataGrid = d as PagingDataGrid; if (pagingDataGrid.IsGenerateColumns) return; int num = 0; if (pagingDataGrid.EnableRowNumber) num++; if (pagingDataGrid.EnableCheckBoxColumn) num++; pagingDataGrid.pagingDataList.GenerateColumns(num, e.NewValue, pagingDataGrid.OperatingKey, pagingDataGrid.OperatingWidth); pagingDataGrid.IsGenerateColumns = true; })); /// <summary> /// 分页控件数据源 /// </summary> public PagingData PagingData { get { return (PagingData)GetValue(PagingDataProperty); } set { SetValue(PagingDataProperty, value); } } // Using a DependencyProperty as the backing store for PagingData. This enables animation, styling, binding, etc... public static readonly DependencyProperty PagingDataProperty = DependencyProperty.Register("PagingData", typeof(PagingData), typeof(PagingDataGrid)); /// <summary> /// 是否启用分页功能 /// </summary> public bool EnablePagination { get; set; } = true; /// <summary> /// 是否启用序号 /// </summary> public bool EnableRowNumber { get; set; } = true; /// <summary> /// 是否复选框列 /// </summary> public bool EnableCheckBoxColumn { get; set; } = false; /// <summary> /// 复选框列是否启用全选功能 /// </summary> public bool EnableSelectAll { get; set; } = false; /// <summary> /// 操作列的Key /// </summary> public string OperatingKey { get; set; } /// <summary> /// 操作列宽 /// </summary> public DataGridLength OperatingWidth { get; set; } /// <summary> /// 当前选中数据 /// </summary> public IEnumerable<object> CheckedList { get { return (IEnumerable<object>)GetValue(SelectedListProperty); } set { SetValue(SelectedListProperty, value); } } // Using a DependencyProperty as the backing store for SelectedList. This enables animation, styling, binding, etc... public static readonly DependencyProperty SelectedListProperty = DependencyProperty.Register("CheckedList", typeof(IEnumerable<object>), typeof(PagingDataGrid), new FrameworkPropertyMetadata(null, FrameworkPropertyMetadataOptions.BindsTwoWayByDefault)); /// <summary> /// 初始化表格行为 /// </summary> private void InintBehavior() { if (!EnablePagination) ucDataGrid.Children.Remove(ucPagination); if (!EnableRowNumber) pagingDataList.Columns.Remove(pagingDataList.Columns.FirstOrDefault(x => x.Header.ToString() == "序号")); if (!EnableCheckBoxColumn) pagingDataList.Columns.Remove(isCheckbox); if (!EnableSelectAll) isCheckbox.Header = "选择"; } /// <summary> /// 用户控件初始化 /// </summary> /// <param name="sender"></param> /// <param name="e"></param> private void UserControl_Loaded(object sender, RoutedEventArgs e) { InintBehavior(); } /// <summary> /// 复选框全选事件 /// </summary> /// <param name="sender"></param> /// <param name="e"></param> private void CheckBox_Click(object sender, RoutedEventArgs e) { var c = sender as CheckBox; CheckedAll(pagingDataList, c.IsChecked); CheckedList = GetSelected(); } /// <summary> /// 全选 /// </summary> /// <param name="parent"></param> /// <param name="isChecked"></param> private void CheckedAll(DependencyObject parent, bool? isChecked) { int numVisuals = VisualTreeHelper.GetChildrenCount(parent); for (int i = 0; i < numVisuals; i++) { DependencyObject v = VisualTreeHelper.GetChild(parent, i); CheckBox child = v as CheckBox; if (child == null) { CheckedAll(v, isChecked); } else { child.IsChecked = isChecked; break; } } } /// <summary> /// 获取所有选中项 /// </summary> /// <returns></returns> private List<object> GetSelected() { List<object> list = new List<object>(); foreach (var item in pagingDataList.ItemsSource) { var m = isCheckbox.GetCellContent(item); var c = m.GetChildObject<CheckBox>("chkItem"); if (c != null && c.IsChecked == true) { list.Add(item); } } return list; } /// <summary> /// 单击选中事件 /// </summary> /// <param name="sender"></param> /// <param name="e"></param> private void chkItem_Click(object sender, RoutedEventArgs e) { CheckedList = GetSelected(); } } }
如何使用动态分页表格
在需要使用的页面(Page)或者控件里面,将WPF用户控件封装后的控件拖进来就可以了,完整的xaml代码如下
<Page x:Class="JHRS.OutpatientSystem.Views.Reservation" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" xmlns:d="http://schemas.microsoft.com/expression/blend/2008" xmlns:local="clr-namespace:JHRS.OutpatientSystem.Views" xmlns:prism="http://prismlibrary.com/" xmlns:b="http://schemas.microsoft.com/xaml/behaviors" prism:ViewModelLocator.AutoWireViewModel="True" xmlns:f="clr-namespace:JHRS.Core.Controls.Layouts;assembly=JHRS.Core" xmlns:p="clr-namespace:JHRS.Core.Controls.DataGrids;assembly=JHRS.Core" xmlns:c="clr-namespace:JHRS.Core.Controls.DropDown;assembly=JHRS.Core" x:Name="page" Title="Reservation" Background="#FFFFFFFF"> <b:Interaction.Triggers> <b:EventTrigger EventName="Loaded" > <b:InvokeCommandAction Command="{Binding LoadedCommand}" CommandParameter="{Binding ElementName=page}" /> </b:EventTrigger> </b:Interaction.Triggers> <Page.Resources> <DataTemplate x:Key="Status"> <c:StatusComboBox Name="cboStatus" SelectedValue="{Binding Status, Mode=TwoWay, UpdateSourceTrigger=Explicit,Converter={StaticResource EnumToIntConverter}}"> <b:Interaction.Triggers> <b:EventTrigger EventName="SelectionChanged" > <b:InvokeCommandAction Command="{Binding DataContext.SelectionChangedCommand,RelativeSource={RelativeSource AncestorType=Page}}" CommandParameter="{Binding ElementName=cboStatus}"/> </b:EventTrigger> </b:Interaction.Triggers> </c:StatusComboBox> </DataTemplate> <DataTemplate x:Key="OperationKey"> <StackPanel Orientation="Horizontal" HorizontalAlignment="Center" VerticalAlignment="Center"> <Button Name="btnEdit" Content="編輯" Background="#00FFFFFF" Style="{StaticResource MaterialDesignOutlinedButton}" Command="{Binding DataContext.EditCommand, RelativeSource={RelativeSource AncestorType=Page}}" CommandParameter="{Binding Path=DataContext, ElementName=btnEdit}" /> <Button Name="btnView" Content="詳情" Margin="10,0,0,0" ToolTip="详情" Command="{Binding DataContext.ViewDetailsCommand, RelativeSource={RelativeSource AncestorType=Page}}" CommandParameter="{Binding Path=DataContext, ElementName=btnView}" /> </StackPanel> </DataTemplate> </Page.Resources> <Grid> <Grid.RowDefinitions> <RowDefinition Height="50" /> <RowDefinition Height="*" /> </Grid.RowDefinitions> <Grid VerticalAlignment="Top"> <f:FunctionArea AddButtonText="新增預約" /> </Grid> <Grid Name="maskContainer" Row="1"> <Grid.RowDefinitions> <RowDefinition /> <RowDefinition Height="5" /> <RowDefinition Height="30" /> </Grid.RowDefinitions> <p:PagingDataGrid Name="ucDataGrid" OperatingKey="OperationKey" OperatingWidth="150*" EnableSelectAll="True" PageData="{Binding PageData}" PagingData="{Binding PagingData}" /> </Grid> </Grid> </Page>
上面代码中<p:PagingDataGrid Name=”ucDataGrid” OperatingKey=”OperationKey” OperatingWidth=”150*” EnableSelectAll=”True” PageData=”{Binding PageData}” PagingData=”{Binding PagingData}” />就是应用动态分页表格的方法,注意绑定数据是你封装的用户控件的PageData依赖属性,而里面绑定的数据源PageData则是ViewModel基类定义的分页数据属性,需要在各子类的ViewModel里面调用接口赋值,如下代码所示:
/// <summary> /// 綁定分頁數據 /// </summary> [WaitComplete] protected async override Task<object> BindPagingData() { var request = this.GetQueryRules(Query); var response = await RestService.For<IReservationApi>(AuthClient).GetPageingData(request); if (response.Succeeded) { PageData = response.Data.Rows; this.PagingData.Total = response.Data.Total; } return response; }
总结一下
合理的进行WPF用户控件封装,可以减少很多重复的代码,本文阐述了怎样封装用户控件的思想,实际项目中,可以结合团队情况和项目状况来封装,总之身处IT江湖,名正言顺的偷偷懒也是向老板多要工资的理由,因为你干活又快又好,哪个包工头不喜欢呢?
下一篇将介绍一下在团队开发中,关于目录文件遵循的一些原则,如果有更好的方式,也欢迎大家提出来。
本系列相关阅读
-
WPF分页控件
2019-06-17 19:44:11WPF分页控件 ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 开发工具与关键技术:C#,WPF 作者:刘海红 撰写时间: 2019年6月12日 ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~...WPF分页控件
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
开发工具与关键技术:C#,WPF
作者:刘海红
撰写时间: 2019年6月12日
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
以前的分页按钮都是用插件来完成的,只需要引用插件就可以用了,但是在WPF中的分页是没有插件的,都是要自己去写和封装的。而我们的WPF的分页用到的是几个控件来实现的页面布局,所以我们也需要初始化这个分页,可以看到下图红色框中的下拉框,它是可以显示所有行数据提供用户选择的,一页有多少行,这个下拉框呢我们可以人为绑定下拉框,绑定我们的数据。
可以看一下如下图中的分页的页面布局代码:
在页面加载事件中加入了当前页面的页数,并且它的最大页数是多少,甚至分页下拉框的显示多少行值用来绑定每页行数的下框
txtCurrentPage.Text = "0";//当前页数
lblMaxPage.Content = decPageCount;//最大页数
int[] intPageLineCounts = { 17, 34, 51, 68, 85, 102 };//创建int数组
cboPageLineCount.ItemsSource = intPageLineCounts;//绑定每页行数下拉框
cboPageLineCount.Text = intPageSize.ToString();//设置默认值
在绑定之前我们要初始化一下全局变量并且用到linq语句查询分页,它也相当于我们sql语句,我们得到是一个字符串紧接着我们用linq语句,根据外界连接,接着我们需要一个排序的字段,为什么要进行排序呢?因为我们等一下要进行分页,要进行分页我们就需要知道有几种可能实现分页?第一种:要知道开始索引,结束索引;第二种:知道开始索引也要知道每一页的条数,以前MVC知道的就是第二种情况。
我们这里的分页操作专门封装到了一个位置里面,首先我们封装一个方法,第一个就是控制按钮的样式,意思就是什么时候该显示按钮什么时候不该显示按钮,不能够点击。以此类推方法都是一样的。
点击上一页和下一页的方法,控制首页的按钮,找到我们的首页给它点击按钮事件,在事件里面控制我们的控件,当它等于1的时候说明它可以用
每一次的方法都会调用到查询,所以我们需要在页面布局中找到查询的事件,并且写出查询的方法,这里查询方法把返回的值赋给了DGV,
查询数据的方法,我们分页的目的就是为了得到后面表格的一个数据,所以我们这里需要注意一下的,它是有返回值的,返回的是一个表格,所以我们直接声明一个方法,在下图代码中我们看到了定义一个方法用来分页的,因为我们返回的数据是一个表格是的数据,返回部分的数据,所以这里返回DataTable
现在我们需要去到服务端进行封装一个公共分页查询的方法,我们创建一个类,这个就叫做PublicFunction,这个名字你可以自己取,但是要和客户端对应,PublicPagingSelect,是数据库中的存储过程。这里的方法我们就封装好了,得到的就是Data数据,这个方法是执行哪一个方法的分页,写完后配置一下服务。
数据库存储过程。
总结:分页过程是很复杂,它不像MVC那样引用一个分页插件,用table标签中的bsgrid来获取数据就可以了。WPF中的分页数据要进行多个步骤。页面布局,后台代码,数据库存储,服务端代码,配置文件…这些都是要经历过的过程。
-
WPF基础知识
2022-03-03 20:44:06此文主要介绍WPF的基础知识,文章会直接摘抄一些优秀文章的表达以及示例用于说明,如果侵犯了作者权利,请联系我速删。编写文章的目的在于记录与分享,方便后续重温与掌握。文章中可能会存在一些小问题,还望各位看...0.前言
此文主要介绍WPF的基础知识,文章会直接摘抄一些优秀文章的表达以及示例用于说明,如果侵犯了作者权利,请联系我速删。编写文章的目的在于记录与分享,方便后续重温与掌握。文章中可能会存在一些小问题,还望各位看官不吝指出。
1.XAML
1.1WPF简介
WPF(Windows Presentation Foundation) 是创建桌面客户端应用程序的UI框架。WPF开发平台支持广泛的应用开发功能,包括应用模型、资源、控件、图形、布局、数据绑定、文档和安全性。WPF是.Net的一部分,WPF 使用 XAML(Extensible Application Markup Language)为应用编程提供声明性模型。
1.2XAML
1.定义
可扩展应用程序标记语言 (XAML) 是一种基于 XML 的声明性语言,可用于创建应用程序UI,其广泛用于以下类型的应用程序:- Windows Presentation Fundation(WPF)应用
- 通用Windows平台(UWP)应用
- Xamarin.Forms应用
2.优势
将界面设计与逻辑编码分离,例如,设计人员可以设计一个UI,然后将 XAML 文件交给开发人员以添加逻辑代码。即使设计人员和开发人员是同一个人(通常如此),程序员也可以将视觉内容保存在 XAML 文件(.xaml)中,而将逻辑代码保存在代码隐藏文件(.cs)中。3.例子
下面的例子中涉及WPF中的一些概念,如布局控件、内容控件、x:前缀、数据绑定等,这些概念后面都会介绍到,例子用于对 XAML 有个初步的认识。<Window x:Class="WPFLearn.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:WPFLearn.Services" mc:Ignorable="d" Title="MainWindow" Height="450" Width="500" Loaded="Window_Loaded"> <Grid> <Grid.RowDefinitions> <RowDefinition Height="234*"/> <RowDefinition Height="134*"/> <RowDefinition Height="67*"/> </Grid.RowDefinitions> <Canvas Grid.Row="0" Margin="0,0,0,67" Grid.RowSpan="3"> <Ellipse x:Name="EllipseRed" Fill="Red" Width="100" Height="60" Canvas.Left="56" Canvas.Top="98" local:TurnoverManager.Angle="{Binding ElementName=SliderAngle,Path=Value}"/> <Rectangle x:Name="RectangleBlue" Fill="Blue" Width="80" Height="80" Canvas.Left="288" Canvas.Top="171" local:TurnoverManager.Angle="45"/> <Button x:Name="ButtonWelcome" Content="欢迎光临" Canvas.Left="265" Canvas.Top="48" FontSize="20" local:TurnoverManager.Angle="60"/> </Canvas> <WrapPanel Grid.Row="2"> <Label Content="角度大小"/> <Slider x:Name="SliderAngle" Minimum="0" Maximum="240" Width="300"/> </WrapPanel> </Grid> </Window>
1.2语法
XAML 语法这一部分基本内容都是摘抄微软官方的WPF中的 XAML 概述,只不过进行了小部分的词语修改以及增加 Demo,降低理解难度。
1.对象元素
对象元素通常声明类型的实例,该类型在将 XAML 用作语言的技术所引用的程序集中定义。指定对象元素标记时,会创建一条指令,指示 XAML 解析器创建基础类型的新实例。每个实例都是在分析和加载 XAML 时通过调用基础类型的无参数构造函数来创建。<Grid> <Button Content="Click Me!"/> </Grid>
注解: Grid、Button都是对象元素。
2.特性语法(属性)
(1)对象的属性通常可表示为对象元素的特性。
(2)对象的特性语法为:property=“value”。
(3)特性的值始终指定为包含在引号中的字符串。<Button Background="Blue" Foreground="Red" Content="Click Me!"/>
注解: Background=“Blue”、Foreground=“Red”、Content=“Click Me!” 都是属性的特性语法。
3.属性元素语法
对于对象元素的某些属性,无法使用特性语法,因为无法在特性语法的引号和字符串限制内充分地为属性值提供所必需的对象或信息。 对于这些情况,可以使用另一个语法,即属性元素语法。<!--使用属性语法代替特性语法--> <Button> <Button.Background> <SolidColorBrush Color="Blue"/> </Button.Background> <Button.Foreground> <SolidColorBrush Color="Red"/> </Button.Foreground> <Button.Content> Click </Button.Content> </Button> <!--使用属性语法创建渐变色--> <Grid > <Rectangle Width="200" Height="200"> <Rectangle.Fill> <LinearGradientBrush StartPoint="0,0" EndPoint="1,1"> <GradientStop Color="Yellow" Offset="0.0" /> <GradientStop Color="Red" Offset="0.25" /> <GradientStop Color="Blue" Offset="0.75" /> <GradientStop Color="LimeGreen" Offset="1.0" /> </LinearGradientBrush> </Rectangle.Fill> </Rectangle> </Grid>
注解:
1.第一个 Button 使用了属性语法来赋值 Background、Foreground、Content ,最终效果与前一点的示例一样。
2.第二个 Button 使用了属性语法来创建渐变色,由于渐变色对象无法在双引号中被表达,所以只能使用这种方式给属性赋值。
3.如果可以使用特性语法给属性赋值的话,就选择特性语法,这样会使内容简洁、编写高效。4.集合语法
XAML 语言包含一些优化,可以生成更易于阅读的标记。其中一项优化是:如果某个特定属性采用集合类型,则在标记中声明为该属性值内的子元素的项目将成为集合的一部分。在这种情况下,子对象元素的集合是设置为集合属性的值。<!--显式声明集合属性--> <Grid > <Rectangle Width="200" Height="200"> <Rectangle.Fill> <LinearGradientBrush StartPoint="0,0" EndPoint="1,1"> <GradientStopCollection> <GradientStop Color="Yellow" Offset="0.0" /> <GradientStop Color="Red" Offset="0.25" /> <GradientStop Color="Blue" Offset="0.75" /> <GradientStop Color="LimeGreen" Offset="1.0" /> </GradientStopCollection> </LinearGradientBrush> </Rectangle.Fill> </Rectangle> </Grid> <!--没有声明集合属性,效果相同--> <Grid > <Rectangle Width="200" Height="200"> <Rectangle.Fill> <LinearGradientBrush StartPoint="0,0" EndPoint="1,1"> <GradientStop Color="Yellow" Offset="0.0" /> <GradientStop Color="Red" Offset="0.25" /> <GradientStop Color="Blue" Offset="0.75" /> <GradientStop Color="LimeGreen" Offset="1.0" /> </LinearGradientBrush> </Rectangle.Fill> </Rectangle> </Grid>
注解: 第二个 Rectangle 设置渐变色时,虽然省略了集合元素 GradientStopCollection ,但 XAML 处理器依旧可以处理得到相同的结果。
5.内容属性
XAML 指定了一个语言功能,通过该功能,类可以指定它的有且仅有一个的属性为 XAML 内容属性。该对象元素的子元素用于设置该内容属性的值。换言之,仅对内容属性而言,可以在 XAML 标记中设置该属性时省略属性元素,并在标记中生成更直观的父级/子级形式。<!--显示声明内容属性--> <Border> <Border.Child> <TextBox Width="300"/> </Border.Child> </Border> <!--没有声明内容属性,效果相同--> <Border> <TextBox Width="300"/> </Border>
注解: 本质同集合语法类似,都是 XAML 处理器进行了一些优化,兼容支持了多种写法,此处是省略表示内容属性的 Border.Child 元素。
6.文本内容
有少量 XAML 元素可直接将文本作为其内容来处理。若要实现此功能,必须满足以下条件之一:- 类必须声明一个内容属性,并且该内容属性必须是可赋值给字符串的类型(该类型可以是Object)。
- 类型必须声明一个类型转换器,该类型转换器将文本内容用作初始化文本。
- 类型必须为已知的XAML语言基元。
<Button>Hello</Button> <Brush>Blue</Brush>
注解: 本质都是 XMAL 处理器的优化处理。
7.特性语法(事件)
特性语法还可用于事件成员,而非属性成员。 在这种情况下,特性的名称为事件的名称。在 XAML 事件的 WPF 实现中,特性的值是实现该事件的委托的处理程序的名称。<Button Click="Button_Click" > Click Me! </Button>
注解: 上述的 XAML 文件关联的 CS 文件中,定义了名称为 Button_Click 的函数。
8.大小写和空白
一般而言,XAML 区分大小写。XAML 中的值并不总是区分大小写:值是否区分大小写将取决于与采用该值的属性关联的类型转换器行为,或取决于属性值类型。例如,采用 Boolean 类型的属性可以采用 true 或 True 作为等效值,但只是因为将字符串转换为 Boolean 的本机 WPF XAML 分析程序类型转换已经允许将这些值作为等效值。
WPF XAML 处理器和序列化程序将忽略或删除所有无意义的空白,并标准化任何有意义的空白,即 XAML 将空格、换行符和制表符转化为空格,如果它们在一个字符串的任一端连续出现,则保留一个空格。
9.标记扩展
标记扩展是一个 XAML 语言概念。在提供特性语法的值时,用大括号({ 和 })表示标记扩展用法。 此用法指示 XAML 处理器不要像通常那样将特性值视为文本字符串或者可转换为字符串的值。
WPF 应用编程中最常用的标记扩展是 Binding(用于数据绑定表达式)以及资源引用 StaticResource 和 DynamicResource 。通过使用标记扩展,即使属性通常不支持特性语法,也可以使用特性语法为属性提供值。标记扩展经常使用中间表达式类型实现一些功能,例如,引用仅在运行时才存在的值。<Window x:Class="index.Window1" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" Title="Window1" Height="100" Width="300"> <Window.Resources> <SolidColorBrush x:Key="MyBrush" Color="Gold"/> <Style TargetType="Border" x:Key="PageBackground"> <Setter Property="BorderBrush" Value="Blue"/> <Setter Property="BorderThickness" Value="5" /> </Style> </Window.Resources> <Border Style="{StaticResource PageBackground}"> <StackPanel> <TextBlock Text="Hello" /> </StackPanel> </Border> </Window>
注解: 以上标记使用特性语法设置 Style 属性的值。Style 属性采用 Style 类的实例,该类在默认情况下无法由特性语法字符串进行实例化。但在本例中,特性引用了特定的标记扩展 StaticResource 。XAML 处理器处理该标记扩展时,将返回在资源字典中实例化的某个样式的引用。
10.根元素和命名空间
一个 XAML 文件只能有一个根元素,以便同时作为格式正确的 XML 文件和有效的 XAML 文件。对于典型 WPF 方案,可使用在 WPF 应用模型中具有突出意义的根元素(例如,页面的 Window 或 Page 、外部字典的 ResourceDictionary 或应用定义的 Application )。
根元素还包含特性 xmlns 和 xmlns:x 。这些特性向 XAML 处理器指示哪些 XAML 命名空间包含标记将其作为元素引用的后备类型的类型定义。 xmlns 特性明确指示默认的 XAML 命名空间。在默认的 XAML 命名空间中,可以不使用前缀指定标记中的对象元素。对于大多数 WPF 应用程序方案以及 SDK 的 WPF 部分中给出的几乎所有示例,默认的 XAML 命名空间均映射到 WPF 命名空间http://schemas.microsoft.com/winfx/2006/xaml/presentation。xmlns:x 特性指示另一个 XAML 命名空间,该命名空间映射 XAML 语言命名空间http://schemas.microsoft.com/winfx/2006/xaml。
只有在每个 XAML 文件的根元素上, xmlns 特性才是绝对必需的。 xmlns 定义将应用于根元素的所有后代元素。 xmlns 属性也允许位于根下的其他元素上,并将应用于定义元素的任何后代元素。 但是,频繁定义或重新定义 XAML 命名空间会导致难以阅读 XAML 标记样式。<Page x:Class="index.Page1" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" Title="Page1"> </Page>
11.x:前缀
常用的 x: 前缀如下:前缀 作用 x:Key 为(或其他框架中的类似字典概念)每个资源设置唯一的键。 x:Class 向为XAML 页提供代码隐藏的类指定 CLR 命名空间和类名。必须具有这样一个类才能支持每个 WPF 编程模型的代码隐藏。 x:Name 处理对象元素后,为运行时代码中存在的实例指定运行时对象名称。通常,经常为 x:Name 使用 WPF 定义的等效属性。此类属性特定映射到 CLR 后备属性,因此更便于进行应用编程,在应用编程中,经常使用运行时代码从初始化的 XAML 中查找命名元素。 最常见的此类属性是 FrameworkElement.Name 。在特定类型中不支持等效的 WPF 框架级属性时,仍然可以使 x:Name 。 x:Type 根据类型名称构造引用。用于指定采用 Type(例如 Style.TargetType)的特性,但属性经常具有本机的字符串到 Type 的转换功能,因此使用 Type 标记扩展用法是可选的。 x:Null 在XAML中指定null值。 注解:
1.指定 Name 的两种方式:Name = “buttonName” 和 x:Name = “buttonName” 。
2.指定 Type 的两种方式:TargetType = “{x:Type Button}” 和 TargetType = “Button” 。
3.x:Null 用于显示指定控件样式为空,来避免指定了类型的样式的全局应用。12.自定义前缀和自定义类型
对于自身的自定义程序集或 PresentationCore、PresentationFramework 和 WindowsBase 的 WPF 核心以外的程序集,可以将该程序集指定为自定义映射的一部分。只要该类型能够正确地实现以支持正在尝试的 XAML 用法,就可以在 XAML 中引用该程序集中的类型。<Page xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:custom="clr-namespace:NumericUpDownCustomControl;assembly=CustomLibrary" > <StackPanel Name="LayoutRoot"> <custom:NumericUpDown Name="numericCtrl1" Width="100" Height="60"/> </StackPanel> </Page>
13.附加属性和附加事件
XAML 指定了一个语言功能,该功能允许对任何元素指定某些属性或事件,即使要设置属性或事件的元素的类型定义中不存在该属性或事件。该功能的属性版本称为附加属性,事件版本称为附加事件。从概念上讲,可以将附加属性和附加事件视为可以在任何 XAML 元素/对象实例上设置的全局成员。
附加属性的最常见方案是使子元素向其父元素报告属性值。<DockPanel Button.Click="DockPanel_Click"> <Button DockPanel.Dock="Left" Width="100" Height="20"> I am on the left </Button> <Button DockPanel.Dock="Right" Width="100" Height="20"> I am on the right </Button> </DockPanel>
注解:
1.上述中的 DockPanel 元素在自身中给 Button.Click 属性事件赋值,而这个属性是不属于 DockPanel 元素的,这便是附加事件。在父元素中处理子元素的事件也体现了 WPF 的另一个特性,即路由事件。路由事件就是在 WP F中,事件会按照元素树向上或向下传递,直到被设置了已处理的标识符。
2.上述中的 Button 元素在自身中给 DockPanel.Dock 属性赋值,而这个属性是不属于 Button 元素的,这便是附加属性。2.控件
2.1控件分类
WPF 中所有控件都继承自 Control ,根据用途不同分为四种类型:布局控件(Panel Controls)、内容控件(Content Controls)、条目控件(Items Controls)、特殊控件(Special Controls)。
2.1.1布局控件
WPF 的布局控件都继承于 System.Windows.Controls.Panel ,常用的布局控件有:Canvas、StackPanel、WrapPanel、DockPanel、Grid、ScrollViewer。
1.Canvas
Canvas 是一个类似于坐标系的面板,所有的元素通过设置坐标来决定其在坐标系中的位置.。具体表现为使用 Left、Top、Right、 Bottom 附加属性在 Canvas 中定位控件。
Canvas 默认不会自动裁剪超过自身范围的内容,即溢出的内容会显示在 Canvas 外面,可以设置 ClipToBounds 属性来裁剪多出的内容。<Canvas Margin="10,10,10,10" Background="White" > <Rectangle Name="rect" Canvas.Left="300" Canvas.Top="180" Fill="Black" Stroke="Red" Width="200" Height="200"/> <Ellipse Name="el" Canvas.Left="160" Canvas.Top="150" Fill="Azure" Stroke="Green" Width="180" Height="180"/> </Canvas>
2.StackPanel
StackPanel 将控件按照行或列来顺序排列,但不会换行。通过设置面板的 Orientation 属性控制两种排列方式:横排(Horizontal)和竖排(Vertical),默认为竖排(Vertical)。<StackPanel Margin="10,10,10,10" Background="Azure"> <Label>A Button Stack</Label> <Button Content="Button 1"></Button> <Button>Button 2</Button> <Button>Button 3</Button> <Button Content="Button 4"></Button> </StackPanel>
3.WrapPanel
WrapPanel 布局面板将各个控件按照一定方向罗列,当长度或高度不够时自动调整进行换行换列。- Orientation=“Horizontal” 时各控件从左至右罗列,当面板长度不够时,子控件就会自动换行,继续按照从左至右的顺序排列。
- Orientation=“Vertical” 时各控件从上至下罗列,当面板高度不够时,子控件就会自动换列,继续按照从上至下的顺序排列。
<WrapPanel Margin="10" Background="Azure"> <Button VerticalAlignment="Top" Margin="5"> Top Button </Button> <Button MinHeight="50"> Tall Button </Button> <Button VerticalAlignment="Bottom"> Bottom Button </Button> <Button> Stretch Button </Button> <Button VerticalAlignment="Center"> Center Button </Button> <Button> Next Button </Button> </WrapPanel>
4.DockPanel
DockPanel 支持让元素简单地停靠在整个面板的某一条边上,然后拉伸元素以填满全部宽度或高度。它也支持让一个元素填充其他已停靠元素没有占用的剩余空间。
DockPanel 有一个 Dock 附加属性,因此子元素用4个值来控制她们的停靠:Left、Top、Right、Bottom 。最后的子元素默认填满所有剩余的空间,除非 DockPanel 的 LastChildFill 属性为 false 。<DockPanel> <Button Content="Button左" DockPanel.Dock="Left"/> <Button Content="Button右" DockPanel.Dock="Right"/> <Button Content="Button上" DockPanel.Dock="Top"/> <Button Content="Button下" DockPanel.Dock="Bottom"/> </DockPanel>
5.Grid
Grid允许我们通过自定义行列来进行布局,这类似于表格。(1)基本使用
- 通过定义Grid的RowDifinitions和ColumnDifinitions来实现对于表格行和列的定义。
- 元素根据附加属性Grid.Row和Grid.Column确定自己的位置。
- 元素根据附加属性Grid.RowSpan和Grid.ColumnSpan占用多行/多列空间。
(2)定义列宽与行高
- 固定长度:属性值为确定的数字。
- 自动长度:属性值为 Auto 。
- 比例长度:属性值为 (number)* ,当 number 为空时表示占用剩余空间,否则按照不同行/列的 number 值进行比例计算后分配空间。
(3)实现拖动方式更改区域尺寸的功能
使用 GidSplitter 可以实现 Grid 行列尺寸重新分配。<Grid> <Grid.RowDefinitions> <RowDefinition Height="40"/> <RowDefinition Height="Auto"/> <RowDefinition Height="6"/> <RowDefinition Height="2*"/> <RowDefinition Height="*"/> </Grid.RowDefinitions> <Button Grid.Row="0" Content="Button"/> <Button Grid.Row="1" Content="Button"/> <GridSplitter Grid.Row="2" HorizontalAlignment="Stretch"/> <Button Grid.Row="3" Grid.RowSpan="2" Content="Button"/> </Grid>
6.ScrollViewer
ScrollViewer 是带有滚动条的面板。在 ScrollViewer 中只能有一个子控件,若要显示多个子控件,需要先将一个附加的 Panel 控件放置在 ScrollViewer 中,然后可以将子控件放置在该控件中。- HorizontalScrollBarVisibility 属性设置水平滚动条是否显示,可选值为 Hidde/Visible/Auto ,默认值为 Hidden ,一般选择 Auto 。
- VerticalScrollBarVisibility 属性设置垂直滚动条是否显示,可选值为 Hidde/Visible/Auto ,默认值为 Visible ,一般选择 Auto 。
<ScrollViewer HorizontalScrollBarVisibility="Auto" VerticalScrollBarVisibility="Auto"> <Button Content="Button" Width="800" Height="800"/> </ScrollViewer>
2.1.2内容控件
内容控件都继承自 ContentControl ,只能容纳一个其他控件作为其内容(需要容纳多个控件时,使用布局控件存放多个控件),内容控件根据是否带标题加以细分:
- 不带标题的内容控件:共同父类是 ContentControl ,常见的控件有:Label、Button 和 Tooltip 等。
- 带标题的内容控件:共同父类是
HeaderedContentControl (继承自 ContentControl ),常见的控件有:TabItem、GroupBox 和 Expander 等。
2.1.3条目控件
条目控件都继承自 ItemsControl ,可以显示一列数据,条目控件根据是否带标题加以细分:
- 不带标题的条目控件:共同父类是 ItemsControl ,常见的控件有:ListBox、Menu 和 StatusBar 等。
- 带标题的条目控件:共同父类是
HeaderedItemsControl (继承自 ItemsControl ),常见的控件有 MenuItem、TreeViewItem 和 ToolBar等。
2.1.4特殊控件
特殊控件没有直接的共同父类,这类控件相对比较独立,常见的控件有:TextBox、TextBlock、Border 和 Image 等。其中,Border 控件是为了实现类似圆角按钮这样的功能,WPF 中的自带按钮本身没有圆角这一属性。
2.1.5综合示例
<Window x:Class="WPFLearn.Views.Control.ControlDemo" 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:WPFLearn.Views.Control" mc:Ignorable="d" Title="ControlDemo" Height="500" Width="800"> <StackPanel Margin="5"> <GroupBox Header="不带标题的内容控件"> <Label Content="我是Labe控件"/> </GroupBox> <GroupBox Header="带标题的内容控件" Margin="0,10,0,0"> <Expander Header="折叠/展开"> <Label Content="可以被折叠的内容"/> </Expander> </GroupBox> <GroupBox Header="不带标题的条目控件" Margin="0,10,0,0"> <ListBox> <ListBoxItem Content="C++"/> <ListBoxItem Content="C#"/> <ListBoxItem Content="Java"/> </ListBox> </GroupBox> <GroupBox Header="带标题的条目控件" Margin="0,10,0,0"> <Menu> <MenuItem Header="文件"> <MenuItem Header="新建"/> <Separator/> <MenuItem Header="保存"/> <MenuItem Header="另存为"/> </MenuItem> </Menu> </GroupBox> <GroupBox Header="特殊控件" Margin="0,10,0,0"> <StackPanel> <TextBox Height="60" BorderBrush="DarkGray"/> <Image Source="/uires/icon.png" Stretch="None" HorizontalAlignment="Left"/> <Border BorderBrush="DarkOrange" BorderThickness="1" CornerRadius="5" Height="60" HorizontalAlignment="Left" Padding="5"> <StackPanel Orientation="Horizontal" VerticalAlignment="Center" Height="30"> <Button Width="90"> 按钮1 </Button> <Button Width="90" Margin="10,0,0,0"> 按钮2 </Button> <Button Width="90" Margin="10,0,0,0"> 按钮3 </Button> </StackPanel> </Border> </StackPanel> </GroupBox> </StackPanel> </Window>
2.2控件外观
XAML 中可以通过三种方式更改控件的外观:
- 为控件的外观属性赋值。
- 为控件创建 Style 。
- 为控件新建 ControlTemplate 。
2.2.1更改外观属性值
许多控件具有支持更改控件外观的属性,常见外观属性有:
- Margin:控件外边距。
- Padding:控件内边距。
- Background:背景颜色,颜色的属性值格式支持常见颜色名称和16进制颜色数值。
- BorderBursh:边框颜色。
- BorderThickness:边框厚度。
- …
<Button Margin="10" Background="#FF0000" BorderThickness="1"/>
2.2.2创建Style
WPF 支持通过创建样式来指定控件的外观,以实现批量设置实例外观的效果。
- 任意元素都有 Resources 属性,在该属性元素下可以创建任意可实例化的资源,比如 WPF 程序集中的类型/C#中的基础类型/…
- 可以在任意 Resources 节点下用 Style 标签创建样式。
- Style 元素的 TargetType 属性可以用两种方式表达:TargetType = “Button” 和 TargetType = {x:Type Button}。
- 如果 Style 元素没有显示指定 TargetType ,则在元素中使用属性时需要加上 Control 前缀。
- 如果 Style 元素只指定 TargetType,而没有指定 x:Key,则样式会自动给样式定义后该类型的所有元素。如果需要取消引用,需要显示指定 Style = {x:Null} 。
- 通过使用属性 BasedOn 实现样式的继承。
2.2.3新建ControlTemplate
在 WPF 中,控件的 ControlTemplate 用于定义控件的外观。可以通过定义新的 ControlTemplate 并将其分配给控件来更改控件的结构和外观。在许多情况下,模板提供了足够的灵活性,从而无需自行编写自定义控件。实现方式:
- 通用:在 Resources 节点下定义 ControlTemplate 元素,然后给控件的 Template 属性设置ControlTemplate 元素的 Key 值。
- 封装:定义样式时,设置 Template 属性,其值用属性元素方式表达 ControlTemplate 元素。
2.3.4综合示例
<Window x:Class="WPFLearn.Views.Control.ControlAppearanceDemo" 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:WPFLearn.Views.Control" mc:Ignorable="d" xmlns:sys="clr-namespace:System;assembly=mscorlib" Title="控件外观演示" Height="450" Width="800"> <Window.Resources> <!--###没有x:Key属性的Style将应用到所有类型为TargetType的实例--> <!--<Style TargetType="Button"> <Setter Property="FontFamily" Value="Times New Roman"/> <Setter Property="FontSize" Value="16"/> </Style>--> <!--###在Resources节点下创建样式,以实现批量设置效果--> <Style x:Key="StyleButton" TargetType="{x:Type Button}"> <Setter Property="FontFamily" Value="Times New Roman"/> <Setter Property="FontSize" Value="16"/> <Style.Triggers> <Trigger Property="IsMouseOver" Value="True"> <Setter Property="FontWeight" Value="Bold"/> </Trigger> </Style.Triggers> </Style> <Style x:Key="StyleButtonWithControlTemplate" TargetType="Button"> <Setter Property="Template"> <Setter.Value> <ControlTemplate TargetType="Button"> <Grid> <Ellipse Name="EllipseSection" Fill="Orange" Width="100" Height="100"/> <ContentPresenter Content="{TemplateBinding Button.Content}" VerticalAlignment="Center" HorizontalAlignment="Center"/> </Grid> <ControlTemplate.Triggers> <Trigger Property="IsMouseOver" Value="True"> <Setter TargetName="EllipseSection" Property="Fill" Value="Yellow"/> </Trigger> </ControlTemplate.Triggers> </ControlTemplate> </Setter.Value> </Setter> </Style> <!--###单独定义ControlTemplate,然后再设置Button.Template属性--> <ControlTemplate TargetType="Button" x:Key="ControlTemplateButton"> <Grid> <Ellipse Name="EllipseSection" Fill="Orange" Width="100" Height="100"/> <ContentPresenter Content="{TemplateBinding Button.Content}" VerticalAlignment="Center" HorizontalAlignment="Center"/> </Grid> <ControlTemplate.Triggers> <Trigger Property="IsMouseOver" Value="True"> <Setter TargetName="EllipseSection" Property="Fill" Value="Yellow"/> </Trigger> </ControlTemplate.Triggers> </ControlTemplate> </Window.Resources> <StackPanel> <GroupBox Header="更改外观属性值"> <StackPanel Orientation="Horizontal"> <StackPanel.Resources> <!--###在任意元素的Resources元素下创建资源,以实现值定义效果--> <FontFamily x:Key="ButtonFontFamily">Times New Roman</FontFamily> <sys:Double x:Key="ButtonFontSize">16</sys:Double> </StackPanel.Resources> <Button FontFamily="Times New Roman" FontSize="16" > StyledButton1 </Button> <Button FontFamily="Times New Roman" FontSize="16" Margin="10,0,0,0"> StyledButton2 </Button> <Button FontFamily="{StaticResource ButtonFontFamily}" FontSize="{StaticResource ButtonFontSize}" Margin="10,0,0,0"> StyledButton3 </Button> <Button Content="StyledButton4" Margin="10,0,0,0"> <Button.Style> <Style> <Style.Setters> <Setter Property="Control.FontFamily" Value="{StaticResource ButtonFontFamily}"/> <Setter Property="Control.FontSize" Value="{StaticResource ButtonFontSize}"/> </Style.Setters> <Style.Triggers> <Trigger Property="Control.IsMouseOver" Value="True"> <Setter Property="Control.FontWeight" Value="Bold"/> </Trigger> </Style.Triggers> </Style> </Button.Style> </Button> </StackPanel> </GroupBox> <GroupBox Header="创建Style实现批量修改外观属性" Margin="0,10,0,0"> <StackPanel Orientation="Horizontal"> <Button Content="StyledButton1"/> <Button Style="{x:Null}" Content="StyledButton2" Margin="10,0,0,0"/> <Button Style="{StaticResource StyleButton}" Content="StyledButton3" Margin="10,0,0,0"/> </StackPanel> </GroupBox> <GroupBox Header="创建ControlTemplate实现自定义控件外观" Margin="0,10,0,0"> <StackPanel Orientation="Horizontal"> <Button Content="NormalButton" VerticalAlignment="Center"/> <Button Content="NewButton1" Style="{StaticResource StyleButtonWithControlTemplate}" Margin="10,0,0,0"/> <Button Content="NewButton2" Template="{StaticResource ControlTemplateButton}" Margin="10,0,0,0"/> </StackPanel> </GroupBox> </StackPanel> </Window>
注解:
1.在 StackPanel.Resources 节点下定义了 FontFamily 类型资源和 sys:Double 类型资源,然后在 Button 元素中通过标记扩展语法引用上述资源,引用语法为 FontFamily="{StaticResource ButtonFontFamily}" FontSize="{StaticResource ButtonFontSize}"。通过这种方式,后续需要修改属性值时,只需要在资源定义处修改,不必改动到多处。
2.如果需要设置控件动态样式,如鼠标移到上面时的样式、鼠标点击时的样式,则需要在 Style 元素下使用属性元素的语法来设置 Style.Triggers 属性的值,Trigger 为触发器。
3.在窗口根元素 Window 节点下定义资源,则该页面所有地方都能引用到该资源。
4.假设 x:Key 为 StyleButton 的样式没有设置 x:Key 值,则页面中所有按钮默认都使用了这个样式,除了显示给 x:Key 赋值 x:Null 的按钮。
5.ControlTemplate 的两种实现方式归根到底是设置控件的 Template 属性,只不过在 Style 元素中设置该值能实现批量修改控件外观的功能。3.绑定
WPF 绑定可以理解为一种关系,该关系告诉 WPF 从一个源对象提取一些信息,并将这些信息来设置目标对象的属性。
- WPF 绑定,必须要有绑定目标和绑定数据源。
- WPF 绑定语法是在 XAML 中使用 {Binding…} 语句,Binding 可以通过 XAML 语句实现界面与数据的耦合,一般情况下,Binding 源是逻辑层的对象,Binding 目标是UI层的控件对象。
- WPF 绑定使得原本需要多行代码实现的功能,现在只需通过简单的 XAML 代码就可以完成。
简单的理解:WPF 绑定特性使得在修改对象的值后,展现对象的界面会自行刷新,不用在代码中手动更新界面展示。如果使用其他UI框架,则可能的执行操作流程是:修改对象的值----更新界面负责展示该值的控件的内容。通过下面的例子可以体会下 WPF 绑定特性的便捷性。
<Window x:Class="WPFLearn.Views.DataBindingDemo" 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:WPFLearn.Views" mc:Ignorable="d" Title="数据绑定之绑定控件" Height="300" Width="400" x:Name="window"> <StackPanel HorizontalAlignment="Stretch" Margin="10"> <ListBox x:Name="ListColor"> <ListBoxItem Content="Blue"/> <ListBoxItem Content="Red"/> <ListBoxItem Content="Green"/> <ListBoxItem Content="Gray"/> <ListBoxItem Content="Cyan"/> </ListBox> <TextBlock Text="{Binding ElementName=ListColor,Path=SelectedItem.Content,Mode=OneWay}" Background="{Binding ElementName=ListColor,Path=SelectedItem.Content,Mode=OneWay}" Margin="0,10,0,0"/> <TextBox Text="{Binding ElementName=ListColor,Path=SelectedItem.Content,Mode=TwoWay}" Background="{Binding ElementName=ListColor,Path=SelectedItem.Content,Mode=OneWay}" Margin="0,10,0,0"/> <Button Margin="0,10,0,0">切换焦点</Button> </StackPanel> </Window>
public partial class DataBindingDemo : Window { public DataBindingDemo() { InitializeComponent(); } }
注解:
1.第一段代码段为窗口的 XAML 文件内容,第二代码段为窗口的 CS 文件内容。
2.该窗口实现的功能:切换列表选择项,下方的文本显示框和文本编辑框的内容和背景色会跟着改变;修改文本编辑框的内容,然后点击切换焦点的按钮,列表选中项的内容会变为文本编辑框中的内容,同时文本显示框的内容、背景颜色与文本编辑框的背景颜色会跟着改变。
3.我们发现在窗口的 CS 文件中,我们没有写任何额外的代码,比如更新控件内容、更新控件背景颜色,我们只是在 XAML 文件中使用了 {Binding…} 这样的绑定语法,便实现了数据同步这一功能,这便是 WPF 绑定的便捷性,也体现出了 WPF 的威力。3.1数据源
WPF 绑定的数据源需要具有通知更改功能,有两种方法设计这种数据源:
- 使用 DependencyObject 类型(依赖属性)。
- 使用实现 INotifyPropertyChanged 接口的类型。
3.2绑定方法
1.ItemsControl
- 在代码中将集合赋值给控件的 ItemsSource 属性,在 XAML 文件中就不用给控件的 ItemsSource 属性赋值。
- 在 XAML 文件中,给控件的 ItemsSource 属性赋值 “{Binding}” ,然后在代码初始化窗口时,将集合作为 DataContext 。
- 在 XAML 文件中,首先使用 ObjectDataProvider 对象标签,设置其 ObjectType、MethodName 属性,这两个属性能够定位提供数据源的方法。然后给控件的 ItemsSource 属性赋值 “{Binding Source={StaticResource DataList}}” 。其中 DataList 为 ObjectDataProvider 标签中的 x:Key 属性的值。
2.ContentControl
在 XAML 文件中,给控件的属性赋值 “{Binding Path=AttributeName}” ,然后在代码初始化窗口时,将窗口本身赋值给 DataContext 。3.3绑定模式
控制绑定机制的关联性,比如单向、双向还是单次…
1.属性:Mode 。
2.枚举值:见下表。枚举值 意义 Default 使用绑定目标的默认 Mode 值。每个依赖项属性的默认值都不同。一般情况下,用户可编辑控件属性(例如文本框和复选框的属性)默认为双向绑定,而多数其他属性默认为单向绑定。 TwoWay 源属性或目标属性的更改可自动更新对方。 OneWay 当绑定源(源)更改时,更新绑定目标(目标)属性。如果无需监视目标属性的更改,则使用 OneWay 绑定模式可避免 TwoWay 绑定模式的系统开销。 OneWayToSource 当目标属性更改时更新源属性。 OneTime 当应用程序启动或数据上下文更改时,更新绑定目标。如果要从源属性初始化具有某个值的目标属性,并且事先不知道数据上下文,则也可以使用此绑定类型。此绑定类型实质上是 OneWay 绑定的简化形式,在源值不更改的情况下可以提供更好的性能。 3.4绑定更新
控制绑定机制触发更新的时机,比如失焦、属性更改…
1.属性:UpdateSourceTrigger 。
2.枚举值:见下表。枚举值 意义 Default 绑定目标属性的默认更新值,大多数依赖项属性的默认值都为 PropertyChanged ,而 Text 属性的默认值为 LostFocus 。 Explicit 仅在调用 UpdateSource 方法时更新绑定源。 LostFocus 当绑定目标元素失去焦点时,更新绑定源。 PropertyChanged 当绑定属性更改时,立即更新绑定源。 3.5示例
<Window x:Class="WPFLearn.Views.DataBinding.BindingDataDemo" 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:WPFLearn.Views.DataBinding" mc:Ignorable="d" x:Name="window" Title="数据绑定之绑定对象" Height="300" Width="400"> <Grid > <StackPanel HorizontalAlignment="Center" VerticalAlignment="Center"> <StackPanel x:Name="PanelStudentInfo"> <StackPanel Orientation="Horizontal"> <TextBlock Text="学号:"/> <TextBlock Text="{Binding Path=ID}" Width="100"/> </StackPanel> <StackPanel Orientation="Horizontal" Margin="0,10,0,0"> <TextBlock Text="姓名:"/> <TextBlock Text="{Binding Path=Name}" Width="100"/> </StackPanel> <StackPanel Orientation="Horizontal" Margin="0,10,0,0"> <TextBlock Text="分数:"/> <TextBlock Text="{Binding Path=Score}" Width="100"/> </StackPanel> <StackPanel Orientation="Horizontal" Margin="0,10,0,0"> <Button Content="改变姓名" Name="ButtonChangeName" Click="ButtonChangeName_Click"/> <Button Content="改变分数" Name="ButtonCHangeScore" Click="ButtonCHangeScore_Click" Margin="20,0,0,0"/> </StackPanel> </StackPanel> <StackPanel Orientation="Horizontal" Margin="0,20,0,0"> <TextBlock Text="学校:"/> <TextBlock x:Name="TextBlockSchool" Text="{Binding Path=School}"/> <Button x:Name="ButtonChangeSchool" Content="改变学校" Width="60" Click="ButtonChangeSchool_Click" Margin="10,0,0,0"/> </StackPanel> </StackPanel> </Grid> </Window>
namespace WPFLearn.Views.DataBinding { /// <summary> /// BindingDataDemo.xaml 的交互逻辑 /// </summary> public partial class BindingDataDemo : Window { //类型1:继承INotifyPropertyChanged的可发通知消息的类型 private Student student; //类型2:依赖属性 public string School { get { return (string)GetValue(SchoolProperty); } set { SetValue(SchoolProperty, value); } } public static DependencyProperty SchoolProperty = DependencyProperty.Register("School", typeof(string), typeof(BindingDataDemo), new PropertyMetadata("清华大学")); public BindingDataDemo() { InitializeComponent(); student = new Student() { ID = 1, Name = "小红", Score = 98 }; //绑定方式1:给元素对象设置DataContext属性,然后在XAML文件中直接引用 this.PanelStudentInfo.DataContext = student; this.TextBlockSchool.DataContext = this; 绑定方式2:手写Binding TODO:存在问题,没办法更新数据,原因暂时不清楚 //Binding bind = new Binding(); //bind.Source = this; //bind.Path = new PropertyPath("School"); //bind.Mode = BindingMode.TwoWay; //bind.UpdateSourceTrigger = UpdateSourceTrigger.PropertyChanged; //this.TextBlockSchool.SetBinding(TextBlock.TextProperty, bind); BindingOperations.SetBinding(this.TextBlockSchool, TextBlock.TextProperty, bind); } private void ButtonChangeName_Click(object sender, RoutedEventArgs e) { student.Name = "小明"; } private void ButtonCHangeScore_Click(object sender, RoutedEventArgs e) { student.Score = 100; } private void ButtonChangeSchool_Click(object sender, RoutedEventArgs e) { School = "北京大学"; } public class Student : INotifyPropertyChanged { private int id; public int ID { get { return id; } set { if (id != value) { id = value; OnPropertyChanged(new PropertyChangedEventArgs("ID")); } } } private string name; public string Name { get { return name; } set { if (name != value) { name = value; OnPropertyChanged(new PropertyChangedEventArgs("Name")); } } } private double score; public double Score { get { return score; } set { score = value; OnPropertyChanged(new PropertyChangedEventArgs("Score")); } } public event PropertyChangedEventHandler PropertyChanged; public void OnPropertyChanged(PropertyChangedEventArgs e) { if (PropertyChanged != null) PropertyChanged(this, e); } } } }
注解:
1.Student 类型实现了 INotifyPropertyChanged,因此 Student 类型的 student 字段可以作为绑定数据源;而窗口 BindingDataDemo 内部添加了 DependencyProperty 类型的依赖属性 School,因此 School 也可以作为绑定数据源。
2.在窗口构造函数中,给控件的数据上下文 DataContext 属性赋值,这样在 XAML 中,控件就可以直接通过 {Binding Path=XXX} 找到XXX所属的对象,然后建立绑定关系。
3.最终,我们在修改函数中,单纯修改内部数据,不用手动更新控件展示值,控件便会自动刷新显示。4.模板
在 WPF 中包含三种模板:控件模板、数据模板和面板模板,它们都继承于 FrameworkTemplate 基类。
4.1控件模板
控件模板(System.Windows.Controls.ControlTemplate),即控件外观,可以通过修改控件模板来自定义控件的外观表现,例如,可以通过修改按钮的控件模板使按钮表现为圆形。控件模板控制UI整体外观。
1.资源定义:ControlTemplate。
2.属性使用:Template。
3.例子:<Window x:Class="WPFLearn.Views.Template.ControlTemplateDemo" 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:WPFLearn.Views.Template" mc:Ignorable="d" Title="模板之控件模板" Height="450" Width="800"> <Window.Resources> <ControlTemplate x:Key="RoundButtonTemplate" TargetType="Button"> <Grid> <Ellipse Name="EllipseSection" Fill="Orange" Width="100" Height="100"/> <ContentPresenter Content="{TemplateBinding Button.Content}" VerticalAlignment="Center" HorizontalAlignment="Center"/> </Grid> <ControlTemplate.Triggers> <Trigger Property="IsMouseOver" Value="True"> <Setter TargetName="EllipseSection" Property="Fill" Value="Yellow"/> </Trigger> </ControlTemplate.Triggers> </ControlTemplate> </Window.Resources> <Grid > <Button Content="Round Button" HorizontalAlignment="Center" VerticalAlignment="Center" Template="{StaticResource RoundButtonTemplate}"/> </Grid> </Window>
4.2数据模板
数据模板(System.Windows.DataTemplate),即数据外观,用于从一个对象中提取数据,并在控件中显示数据。只有内容控件与条目控件支持数据模板。数据模板控制UI内容形式。
1.资源定义:DataTemplate 。
2.属性使用:ContentTemplate(内容控件)、ItemTemplate(条目控件)。
3.例子:<Window x:Class="WPFLearn.Views.Template.DataTemplateDemo" 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:WPFLearn.Services" mc:Ignorable="d" Title="模板之数据模板" Height="450" Width="800"> <Window.Resources> <DataTemplate x:Key="PersonDataTemplate"> <Border Name="BorderBlue" Margin="3" BorderThickness="3" BorderBrush="Blue" CornerRadius="5"> <Grid Margin="3"> <Grid.RowDefinitions> <RowDefinition/> <RowDefinition/> </Grid.RowDefinitions> <TextBlock x:Name="TextBlockName" FontWeight="Bold" Text="{Binding Path=Name}"/> <TextBlock Grid.Row="1" Text="{Binding Path=Age}"/> </Grid> </Border> <DataTemplate.Triggers> <Trigger SourceName="BorderBlue" Property="IsMouseOver" Value="True"> <Setter TargetName="BorderBlue" Property="Background" Value="LightGray"/> <Setter TargetName="TextBlockName" Property="FontSize" Value="20"/> </Trigger> </DataTemplate.Triggers> </DataTemplate> </Window.Resources> <Grid Margin="0,0,0,10"> <Grid.RowDefinitions> <RowDefinition/> <RowDefinition Height="60"/> </Grid.RowDefinitions> <ListBox x:Name="ListBoxPerson" ItemsSource="{Binding}" HorizontalContentAlignment="Stretch" ItemTemplate="{StaticResource PersonDataTemplate}"/> <StackPanel Grid.Row="1" Orientation="Horizontal" HorizontalAlignment="Center" Margin="0,10,0,0"> <Button x:Name="ButtonAddAgeOfFirstItem" Content="递增第一项年龄值" Height="30" Click="ButtonAddAgeOfFirstItem_Click"/> <Button x:Name="ButtonDeleteLastItem" Content="删除第二项" Height="30" Margin="10,0,0,0" Click="ButtonDeleteLastItem_Click"/> </StackPanel> </Grid> </Window>
4.3面板模板
面板模板(System.Windows.Controls.ItemsPanelTemplate),即面板外观,而面板又是用于进行布局的,所以面板的外衣也就是布局的外衣,通过修改面板模板可以自定义控件的布局。例如,ListBox默认是自从向下地显示每一项的,此时可以通过修改面板模板使其自左向右地显示每一项。面板模板控制UI内容布局。
1.资源定义:ItemsPanelTemplate 。
2.属性使用:ItemsPanel 。
3.例子:<Window x:Class="WPFLearn.Views.Template.PanelTemplateDemo" 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:WPFLearn.Views.Template" mc:Ignorable="d" Title="模板之面板模板" Height="450" Width="800"> <Window.Resources> <DataTemplate x:Key="PersonDataTemplate"> <Border x:Name="BorderBlue" Margin="3" BorderThickness="3" BorderBrush="Blue" CornerRadius="5"> <Grid Margin="3"> <Grid.RowDefinitions> <RowDefinition/> <RowDefinition/> </Grid.RowDefinitions> <TextBlock x:Name="TextBlockName" FontWeight="Bold" Text="{Binding Name}"/> <TextBlock Grid.Row="1" Text="{Binding Age}"/> </Grid> </Border> <DataTemplate.Triggers> <Trigger SourceName="BorderBlue" Property="IsMouseOver" Value="True"> <Setter TargetName="BorderBlue" Property="Background" Value="LightGray"/> <Setter TargetName="TextBlockName" Property="FontSize" Value="20"/> </Trigger> </DataTemplate.Triggers> </DataTemplate> <ItemsPanelTemplate x:Key="ListItemsPanelTemplate"> <StackPanel Orientation="Horizontal" VerticalAlignment="Center" HorizontalAlignment="Center"/> </ItemsPanelTemplate> </Window.Resources> <Grid> <ListBox x:Name="ListBoxPerson" ItemsPanel="{StaticResource ListItemsPanelTemplate}" ItemTemplate="{StaticResource PersonDataTemplate}"/> </Grid> </Window>
5.资源
WPF 资源系统是一种保管一系列对象(如常用的画刷、样式或模板)的简单办法,从而让使用者更容易地复用这些对象。
5.1资源定义
每一个框架级元素( FrameworkElement 或者 FrameworkContentElement )都有一个资源属性。每一个在资源字典中的资源都有一个唯一不重复的键值( key ),在标签中使用 x:Key 属性来标识它。一般地,键值是一个字符串,但你也可以用合适的扩展标签来设置为其他对象类型。非字符键值资源使用于特定的WPF 区域,尤其是风格、组件资源以及样式数据等。
5.2资源使用
为了使用 XAML 标记中的资源,需要一种引用资源的方法,可以通过两个标记来引用资源:StaticResource 和 DynamicResource 。
- StaticResource :以静态方式引用资源。在第一次创建窗口时,一次性地设置完毕,程序运行过程中资源发生变化,程序不会更新引用资源的对象。无法实时更新程序,效率高。
- DynamicResource :以动态方式引用资源。如果资源发生了改变,程序会重新应用资源。可以实时更新程序,效率低。
5.3资源范围
WPF 提供一个封装和存取资源的机制,我们可将资源建立在应用程序的不同范围上。WPF中,资源定义的位置决定了该资源的可用范围,程序会按照范围由小到大的顺序检索资源。资源按照范围分类见下表。
级别 说明 物件级 资源定义在普通元素下,该资源只能套用在这个元素及其其子元素。 文件级 资源定义在Window或Page元素下,该资源可以套用到这个文件中的所有元素。 应用程序级 资源定义在App.xaml中,该资源可以套用到应用程序内的任何地方。 字典级 将资源封装成资源字典定义在资源XAML文件中,该资源可以在套用到其他应用程序中。 系统级 资源检索的最终层级,我们无法在这层级上自定义资源,系统级资源有系统颜色设置/字体设置/屏幕标准尺寸等。 5.4综合示例
1.StaticResource 和 DynamicResource 的区别
<Window x:Class="WPFLearn.Views.ResourceStyle.ResourceStyleDemo" 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:WPFLearn.Views.ResourceStyle" mc:Ignorable="d" Title="ResourceStyleDemo" Height="300" Width="300"> <Window.Resources> <SolidColorBrush x:Key="RedBrush" Color="Red"></SolidColorBrush> </Window.Resources> <StackPanel Margin="5"> <StackPanel> <Button Background="{StaticResource RedBrush}" Margin="5" FontSize="14" Content="Use Static Resource"/> <Button Background="{DynamicResource RedBrush}" Margin="5" FontSize="14" Content="Use Dynamic Resource"/> <Button Margin="5" FontSize="14" Content="Change the RedBrush to Yellow" Click="Button_Click"/> </StackPanel> </StackPanel> </Window>
public partial class ResourceStyleDemo : Window { public ResourceStyleDemo() { InitializeComponent(); } private void Button_Click(object sender, RoutedEventArgs e) { this.Resources["RedBrush"] = new SolidColorBrush(Colors.Yellow); } }
注解: 点击按钮后,Use Static Resource 的按钮背景颜色没改变,Use Dynamic Resource 的按钮背景颜色改变。
2.各种级别的资源
物件级和文件级两个级别的资源之前的示例都有见过,下面是字典级别的,在 App.xaml 引用字典级别资源,相当于定义了应用级别资源,另外,应用级别资源定义同其它级别资源定义一样,所以应用级别资源也不做演示。<!--CheckBoxStyle.xaml--> <ResourceDictionary xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"> <Style x:Key="CheckBoxStyle" TargetType="{x:Type CheckBox}"> <Setter Property="IsChecked" Value="False"/> <Setter Property="Background" Value="Transparent"/> <Setter Property="Template"> <Setter.Value> <ControlTemplate TargetType="CheckBox"> <DockPanel Background="{TemplateBinding Background}" ToolTip="{TemplateBinding Content}" LastChildFill="False" Width="{TemplateBinding Width}"> <Image Margin="0" DockPanel.Dock="Left" x:Name="CheckImage" Stretch="Fill" Source="/Uires/复选框未选.png" Width="15" Height="15"/> <TextBlock DockPanel.Dock="Left" Foreground="{TemplateBinding Foreground}" Margin="2,0,0,0" VerticalAlignment="Center" Text="{Binding Content,RelativeSource={RelativeSource TemplatedParent}}"/> </DockPanel> <ControlTemplate.Triggers> <Trigger Property="IsChecked" Value="True"> <Setter TargetName="CheckImage" Property="Source" Value="/Uires/复选框已选.png"/> </Trigger> </ControlTemplate.Triggers> </ControlTemplate> </Setter.Value> </Setter> </Style> </ResourceDictionary>
<!--Style.xaml--> <ResourceDictionary xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"> <ResourceDictionary.MergedDictionaries> <ResourceDictionary Source="pack://application:,,,/Styles/ButtonStyle.xaml"/> <ResourceDictionary Source="pack://application:,,,/Styles/CheckBoxStyle.xaml"/> <ResourceDictionary Source="pack://application:,,,/Styles/OtherStyle.xaml"/> </ResourceDictionary.MergedDictionaries> </ResourceDictionary>
<!--App.xaml--> <Application x:Class="WPFLearn.App" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:local="clr-namespace:WPFLearn" StartupUri="Views/Control/ControlClassificationDemo.xaml"> <Application.Resources> <ResourceDictionary> <ResourceDictionary.MergedDictionaries> <ResourceDictionary Source="pack://application:,,,/Styles/Style.xaml"/> </ResourceDictionary.MergedDictionaries> </ResourceDictionary> </Application.Resources> </Application>
注解:
1.CheckBoxStyle.xaml 定义了 CheckBox 样式,Style.xaml 包含所有控件样式文件,App.xaml 引入 Style.xaml。
2.CheckBoxStyle.xaml 和 Style.xaml 都定义了字典级的资源,在 App.xaml 中定义的资源便是应用程序级的资源。6.样式
WPF 中通过样式可以方便地批量设置控件外观。
6.1样式定义
在 XAML 中通过使用 Style 元素定义样式,既可以在控件元素中定义样式(样式用于控件自身),也可以在 Resource 元素中使定义样式(样式可以被范围内的所有控件引用)。
在 Style 元素中,通过 Style.Setters 元素定义样式静态内容,通过 Style.Triggers (触发器)元素定义样式动态内容。
1.WPF中的触发器类型
(1)属性触发器(Property Trigger):当Dependency Property的值发生改变时触发。Trigger 和 MultiTriiger 。
(2)数据触发器(Data Trigger):当普通.NET属性的值发生改变时触发。DataTrigger 和 MultiDataTrigger。
(3)事件触发器(Event Trigger):当路由事件发生时触发。EventTrigger 。注解: 触发器的 Multi 版本适用多条件的场景,比如设置复选框 选中状态+鼠标移动到上面 的样式。
6.2样式使用
1.在控件元素中定义的样式
不需要显示引用,效果相当直接在控件元素中给各种样式属性赋值。
2.在Resource元素中定义样式- x:Key 属性为空的样式:样式通过 TargetType 属性指定控件类型,该类型的所有控件实例都会隐式引用该样式。如果想要取消引用,需要显示地给样式属性赋值 Style = “{x:Null}” 。
- x:Key 属性不为空的样式:需要显示地给控件元素的样式属性赋值 Style = “{StaticResouce StyleKey}” 。
7.路由事件
WPF 除了创建了一个新的依赖属性系统之外,还用更高级的路由事件功能替换了普通的 .NET 事件。
路由事件是具有更强传播能力的事件——它可以在元素树上向上冒泡和向下隧道传播,并且沿着传播路径被事件处理程序处理。<Border Height="50" Width="300" BorderBrush="Gray" BorderThickness="1"> <StackPanel Background="LightGray" Orientation="Horizontal" Button.Click="CommonClickHandler"> <Button Name="YesButton" Width="Auto" >Yes</Button> <Button Name="NoButton" Width="Auto" >No</Button> <Button Name="CancelButton" Width="Auto" >Cancel</Button> </StackPanel> </Border>
private void CommonClickHandler(object sender, RoutedEventArgs e) { FrameworkElement feSource = e.Source as FrameworkElement; switch (feSource.Name) { case "YesButton": // do something here ... break; case "NoButton": // do something ... break; case "CancelButton": // do something ... break; } e.Handled=true; }
8.命令模式
1.概述
在现在的用户界面中,常常需要从多个不同位置访问通过函数,即由不同的动作触发同样响应。在这种情况下,如果使用路由事件,则需要定义多个事件,然后在这些事件中调用同个函数,这样处理会很繁琐。WPF 使用命令机制来解决上述问题:WPF 允许在一个地方定义命令,并且在所有的用户接口控件之中根据需要地调用这些命令,例如 Menu,ToolBar 按钮等,不同动作最终会得到相同的响应。2.WPF 中的命令模型
- 命令:命令表示一个程序任务,并且可跟踪该任务是否能被执行。然而,命令实际上不包含执行应用程序的代码,真正处理程序在命令目标中。
- 命令源:命令源触发命令,即命令的发送者。例如 Button、MenuItem 等控件都是命令源,单击它们都会执行绑定的命令。
- 命令目标:命令目标是在其中执行命令的元素。如 Copy 命令可以在 TextBox 控件中复制文本。
- 命令绑定:命令是不包含执行程序的代码的,真正处理程序存在于命令目标中。那命令是怎样映射到处理程序中的呢?这个过程就是通过命令绑定来完成的,命令绑定完成的就是红娘牵线的作用。
3.WPF 命令的核心
ICommand 接口,该接口定义了命令的工作原理。9.MVVM
1.概述
MVVM 是 WPF 衍生出的一种优秀编程框架(模式),这种模式充分利用了 WPF 的数据绑定机制,最大限度地降低了 XAML 文件和 CS 文件的耦合度,实现了将视图UI和业务逻辑分离的效果。在需要更换界面时,逻辑代码修改很少,甚至不用修改。
2.定义- MVVM 编程模式通常被用于 WPF 或 Silverlight 开发,一些现代的 WEB 前端框架也使用了这种模式。
- MVVM 是 Model-View-ViewModel 的缩写形式。
模型(Model):数据对象,负责存储数据。
视图(View):界面对象,负责与用户交互、接收用户输入、把数据展现给用户。
视图模型(View Model):连接对象,负责连接 Model 层和 View 层。View Model 不仅仅是 Model 的包装,它还包含了程序逻辑以及 Model 扩展。
3.优点
- 低耦合:视图(View)可以独立于 Model 变化和修改,一个 ViewModel 可以绑定到不同的 View 上,当 View 变化的时候 Model 可以不变,当 Model 变化的时候 View 也可以不变。
- 可重用性:可以把一些视图逻辑放在一个 ViewModel 里面,让很多 View 重用这段视图逻辑。
- 独立开发:开发人员可以专注于业务逻辑和数据的开发(ViewModel),设计人员可以专注于页面设计,使用 Expression Blend 可以很容易设计界面并生成XAML代码。
4.在 WPF 中使用 MVVM 时的特点
- 视图的 CS 文件包含极少的代码,其核心逻辑都被放在 View Model 类中,从而使得程序逻辑与视图耦合度降低。
- ViewModel 类作为 View 的 DataContext 。
- 在 MVVM 下,所有事件和动作都被当成命令,如按钮的点击操作,此时不是触发点击事件,而是绑定到一个点击命令,再由命令去执行对应的逻辑代码。
5.示例
WPF快速入门系列(8)——MVVM快速入门,这位作者写的这轮系列文章都很不错,推荐大家入门 WPF 时去看,地址是WFP快速入门系列 | Learning hard | 博客园。10.参考资料
1.先说下自己推荐的学习路线:
(1)先用1.5倍速看一遍《深入浅出WPF》系列高清视频教程 | 刘铁猛 | B站 的视频。
(2)再认真把 WFP快速入门系列 | Learning hard | 博客园 的系列文章都过一遍。
(3)最后找个现有的客户端程序进行模仿练习,用到哪些控件再上网查下具体用法。2.参考资料大致如下:
《深入浅出WPF》系列高清视频教程 | 刘铁猛 | B站
WFP快速入门系列 | Learning hard | 博客园
WPF入门教程系列 | DotNet菜园 | 博客园
WPF基础之资源 | SmilelyCoding | 博客园
WPF教程 | vue5.com在线教程 -
WPF 实践
2021-05-08 08:59:10在 WPF 设计阶段的初期,会有许多辩论,其中应该在系统的托管组件和非托管组件之间绘制线条。 CLR 提供了许多功能,这些功能使得开发更高效、更可靠 (包括内存管理、错误处理、通用类型系统等 ) ,但会产生费用。 ... -
WPF开发教程
2019-07-02 23:13:20------WPF开发教程 目录 WPF基础入门.... 3 1. WPF基础之体系结构... 3 2. WPF基础之XAML. 9 3. WPF基础之基元素... 23 4. WPF基础之属性系统... 26 5. WPF基础之路由事件... 33 6. WPF基础之布局系统... ... -
WPF—自定义分页
2022-05-24 13:40:57WPF DataGrid 分页DataGridDataGrid 美化DataGrid 数据源绑定DataGrid 分页页面调用分页 WPF DataGrid 分页 DataGrid DataGrid 数据列表 看一下DataGrid的独特属性: AlternationCount:设置ItemControl中... -
(更新时间)2021年11月18日 WPF 2021秋招WPF高频面试题
2021-11-18 21:39:492. 说说WPF中的XAML是什么?为什么需要它?它只存在于WPF吗?WPF初级篇[13]3.简单描述下WPF的样式4.WPF 中的资源是什么?5.WPF中的Visibility.Collapsed和Visibility.Hidden有什么区别?6.什么是静态资源和动态资源?... -
toolbar wpf 按钮带文字_Tob设计:中台设计组件按钮
2020-12-06 12:41:09希望接下来每一篇关于中台组件的分享对大家有所帮助,本片文章分享关于按钮的发展史、按钮的类型、按钮的使用规范等内容。巨量引擎是字节跳动旗下的营销服务品牌,服务于字节跳动的商业化。基于国内广告产品不断优化... -
WPF 入门(一)
2021-08-02 20:02:22参考书籍《深入浅出WPF》-- 刘铁锰 二、WPF定义 WPF全称:Windows Presentation Foundation WPF是一个可创建桌面客户端应用程序的 UI 框架。 WPF 开发平台支持广泛的应用开发功能,包括应用模型、资源、控件、... -
《深入浅出WPF》——模板学习
2022-05-19 10:44:52“形而上者谓之道”指的就是基于现实世界对万物进行抽象封装、理顺它们之间的关系,这个“道”不就是面向对象的思想吗!如果把面向对象思想进一步提升、总结出对象之间的组合关系,“道”就继续上升为设计模式思想... -
toolbar wpf 按钮带文字_字节跳动商业化中台系统设计组件按钮
2020-12-04 07:53:03点击上方蓝字关注+ 点击右上方“...”设为星标嗨小伙伴们大家周末好~☀️今天小编给大家带来的文章是关于字节跳动商业化「中台系统」的按钮组件知识点,文章分享关于按钮的发展史、按钮的类型、按钮的使用规范等... -
WPF管理系统开发框架搭建指南,2020从入门到放弃
2020-12-21 17:39:44WPF技术是一个很不错的技术,但一直没有上手过正式的项目,趁在做这个医疗项目时,遂搭建一个WPF开发框架,目的是为了统一WPF开发并提高开发效率;我对WPF技术算是零基础,现学现卖,用这些不成体系的文字予以记录... -
WPF学习日记:持续修改
2020-04-16 10:41:23一、wpf基本知识 在VS中自动引用: PresentationCore PresentationFramework WindowsBase 也可以自己用文本创建工程: 1、打开VS编译器(与cmd类似但是已经加载一些环境设置): 2、引用上述核心依赖:用\r 来开始 ... -
(四)开源C# WPF控件库《AduSkin – UI》
2020-02-22 11:35:24微信公众号:【Dotnet9的博客】,网站:【Dotnet9】,问题或建议:【请网站留言】,...一款简单漂亮的WPF UI,融合部分开源框架的组件,为个人定制的UI,可供学者参考。 阅读导航 关于《AduSkin》 1.1 控件库全貌... -
推荐一个IT老鸟肝了2月有余的免费开源WPF企业级开发框架
2020-09-12 08:00:00JHRS一个新学WPF的IT老鸟,肝了2个月做了这么一个WPF企业级开发框架,站长clone学习,觉得甚是不错。这是一个使用了Prism搭建的插件式框架,封装了DataGrid的使用,使... -
表格绑定数据、模糊查询(下拉框)与动态时间:
2021-12-19 18:15:10JTable,表格。JTable 是用来显示和编辑常规二维单元表。 2.JTable常用构造方法 4.1 创建空表格,后续再添加相应数据 JTable() 4.2 创建指定行列数的空表格,表头名称默认使用大写字母(A, B, C ...)依次表示... -
《Dotnet9》系列-开源C# WPF控件库3《HandyControl》强力推荐
2019-12-08 20:50:09《HandyControl》Pagination ,表格等常用的分页封装控件,大数据分页展示使用很方便,分页控件样式也是比较流行。 《HandyControl》Pagination 4.2 标签类控件 《HandyControl》提供的标签类控件较多,... -
wpf
2016-04-18 16:03:001.WPF 设置TextBox为空时,背景为文字提示。 <TextBox FontSize="17" Height="26" Margin="230,150,189,0" Name="txt_Account" VerticalAlignment="Top" Foreground="Indigo" TabIndex="0" ... -
WPF X18的TX Text Control .NET v28
2021-02-03 21:43:07WPF X18的TX Text Control .NET v28.0.2100.500 WPF X18的TX Text报告和邮件合并 文本控制报告结合了报告工具和易于使用的所见即所得文字处理器的功能-完全可编程且可嵌入到WPF应用程序中。 基于与MS Word... -
WPF 柱状图报表饼图报表区域折线图报表横向柱状图报表折线图报表
2017-04-01 18:13:43WPF 柱状图报表饼图报表区域折线图报表横向柱状图报表折线图报表 -
实现一个内容超出显示省略号,并鼠标浮入显示tooltip,不超出的不显示tooltip组件
2021-09-22 15:03:50实现一个内容超出显示省略号,并鼠标浮入显示tooltip,不超出的不显示tooltip组件 ps:该组件是基于element-plus,使用vue3最新的setup语法糖实现的。不清楚的大家可以根据我的思路用其他技术栈实现。 背景 项目中... -
在Java窗体表格中插入复选框
2021-02-26 09:28:3157 58 //开始向表格中添加复选框(注意:此示例较为简单,缺省很多判断,也没有动态代码支持)59 //通过设置列渲染60 61 //方法一:直接方式 使用TableColumn的setCellRenderer方法(推荐)62 //此方法可以设置某一... -
Vue UI组件 开发框架 服务端 辅助工具 应用实例 Demo示例
2018-08-31 10:54:10Vux ★7503 - 基于Vue和WeUI的组件库 iview ★5801 - 基于 Vuejs 的开源 UI 组件库 mint-ui ★5517 - Vue 2的移动UI元素 vue-material ★2790 - 通过Vue Material和Vue 2建立精美的app... -
WPF Step By Step -基础知识介绍
2018-06-27 14:20:12本文大纲 什么是xaml 什么是路由事件 WPF都提供了那些基础控件 什么是依赖属性 元素绑定 WPF中的资源 WPF的几种布局方式什么是Xaml Xaml(Extensible Application Markup Language) 可扩展应用程序标记语言,... -
Web应用组件化的权衡
2016-07-02 09:45:341. 基本概念 什么是Web应用? 所谓Web应用,指的是那些虽然用Web技术构建,但是展现形式却跟桌面程序或者移动端原生应用类似的产品。这类产品的特点是逻辑较重,交互复杂,通常也是单页式的。...组件