当前位置: 首页 > news >正文

WPF命令

在设计良好的Windows应用程序中,应用程序逻辑不应位于事件处理程序中,而应在更高层的方法中编写代码。其中的每个方法都代表单独的应用程序任务。每个任务可能依赖其他库。

使用这种设计最明显的方式是在需要的地方添加事件处理程序,并使用各个事件处理程序调用恰当的应用程序方法。本质上,窗口代码变成了一个精简的交换台,可以响应输入,并将请求转发到应用程序。

尽管这种设计非常合理,但却没有减少任何工作。许多应用程序任务可通过各种不同的路由触发,所以经常需要编写多个时间处理程序来调用相同的应用程序方法。就其本身而言,这并不是什么问题(因为交换台代码非常简单),但当需要处理用户界面的状态时,问题就变复杂了。

设想有一个程序,该程序包含方法 PrintDocument()。使用4中方式触发该方法:通过主菜单(选择File|Print菜单项),通过上下文菜单(在某处右击并选择Print菜单项),通过键盘快捷键(Ctrl+P)以及通过工具栏。在应用程序生命周期的特定时刻,需要暂时禁用PrintDocument() 任务。这意味着需要禁用两个菜单选项和一个工具栏按钮,使他们不能被单击,并需要忽略 Ctrl+P快捷键。编写代码完成这些工作(并在后面添加代码以启用这些控件)是很麻烦的,更糟的是,如果没有正确完成这项工作,可能会使不同状态的代码块不正确的相互重叠,从而导致某个控件在可应该可用时而被启用。编写和调试这类代码是Windows开发中最枯燥的内容之一。

幸运的是,WPF使用新的命令模型帮助解决这些问题。它增加了两个重要特性:

1、将事件委托到适当的命令

2、使控件的启用状态和相应的命令状态保持同步

命令模型

WPF命令模型由许多可变的部分组成。总之,它们都具有如下4个重要元素:

命令 命令表示应用程序任务,并且跟踪任务是否能够被执行。然而,命令实际上不包含执行应用程序任务的代码。

命令绑定 每个命令绑定针对用户界面的具体区域,将命令连接到相关的应用程序逻辑。这种分解的设计是非常重要的,因为单个命令可用于应用程序中的多个地方,并且在每个地方具有不同的意义。为处理这一问题,需要将同一命令与不同的命令源绑定。

命令源 命令源触发命令。例如,MenuItem和Button都是命令源。单击它们都会执行绑定命令。

命令目标 命令目标是在其中执行命令的元素。例如,Paste命令可在TextBox 控件中插入文本,而OpenFile 命令可在DocumentViewer中打开文档。根据命令的本质,目标可能很重要,也可能不重要。

ICommand接口

WPF命令模型的核心是 System.Windows.Input.ICommand 接口,该接口定义了命令的工作原理。

    public interface ICommand{event EventHandler? CanExecuteChanged;bool CanExecute(object? parameter);void Execute(object? parameter);}

在一个简单实现中,Execute()方法将包含应用程序任务逻辑。然而,WPF的实现更复杂,它使用Execute()方法引发一个更复杂的过程,该过程最终触发在应用程序其他地方处理的事件。通过这种方式可以使用预先准备好的命令类,并插入自己的逻辑。还可以灵活地在几个不同的地方使用同一个命令。

CanExecute()方法返回命令的状态——如果命令可用,就返回true;如果不可以,就返回false。Execute() 和 CanExecute()方法都接受一个附加的参数对象,可使用该对象传递所需的任何附加信息。

最后,当命令状态改变时,引发CanExecuteChanged事件。对于使用命令的任何控件,这是指示信号,表示他们应当调用CanExecute()方法检查命令的状态。通过使用该事件,当命令可用时,命令源可自动启用自身;当命令不可用是,禁用自身。

RoutedCommand

当创建自己的命令时,不会直接实现ICommand接口,而是使用System.Windows.Input.RoutedCommand类,该类自动实现ICommand接口。RoutedCommand类是WPF中唯一实现了ICommand接口的类。换句话说,所有WPF命令都是RoutedCommand类及其派生类的实例。

RoutedCommand类不包含任何应用程序逻辑,而只代表命令,这意味着各个RoutedCommand对象具有相同的功能。

RoutedCommand类为事件冒泡和隧道添加了一些额外的基础结构。鉴于ICommand接口封装了命令的思想——可被触发的动作并可被启用或禁用——RoutedCommand类对命令进行的了修改,使命令可在WPF元素层次结构中冒泡,以便获得正确的事件处理程序。

为支持路由时间,RoutedCommand类私有的实现了ICommand接口,并添加了RoutedCommand接口方法的一些不同版本。最明显的变化是,Execute() 和 CanExecute() 方法使用了一个额外参数。

public bool CanExecute(object parameter, IInputElement target);
public void Execute(object parameter, IInputElement target);

参数target是开始处理事件的元素。事件从target元素开始,然后冒泡至高层的容器,知道应用程序为了执行合适的任务而处理了事件(为了处理Executed事件,元素还需要借助于另一个类——CommandBinding类的帮助)。

除上面的修改外,RoutedCommand类还引入了三个属性:命令名称(Name)、包含命令的类(OwnerType)以及任何可用于触发命令的按键或鼠标操作(位于InputGestures集合中)。

RoutedUICommand

在程序中处理的大部分命令不是RoutedCommand对象,而是RoutedUICommand类的实例,RoutedUICommand类继承自RoutedCommand类(实际上,WPF提供的所有预先构建好的命令都是RoutedUICommand对象)。

RoutedUICommand类用于具有文本的命令,这些文本显示在用户界面中的某些地方。RoutedUICommand类只增加了Text属性,该属性是为命令显示的文本。

为命令定义命令文本(而不是在控件上定义文本)的优点是可以在某个位置执行本地化。但如果命令文本永远不会在用户界面的任何地方显示,那么RoutedUICommand类和RoutedCommand类是等效的。

命令库

WPF设计者认识到,每个应用程序可能有大量的命令,并且对于许多不用的应用程序,很多命令是通用的。为减少创建这些命令所需的工作,WPF提供了基本命令库,基本命令库中保存的命令超过100条。这些命令通过以下5个专门的静态类的静态属性提供:

ApplicationCommands 该类提供通用命令,包括剪贴板命令(如Copy、Cut和Paste)以及文档命令(如New、Open、Save、SaveAs和Print等)。

NavigationCommands 该类提供了用于导航的命令,包括基于页面的应用程序设计的一些命名(如BrowseBack、BrowseForward和NextPage),以及其他适合于基于文档的应用程序的命令(如IncreaseZoom和Refresh)。

EditingCommands 该类提供了许多重要的文档编辑命令,包括用于移动的命令(MoveToLineEnd、MoveLeftByWord和MoveUpByPage等),选择内容的命令(SelectToLineEnd、SelectLeftByWord),以及改变格式的命令(ToggleBold和ToggleUnderline)。

ComponentCommands 该类提供了由用户界面组件使用的命令,包括用于移动和选择内容的命令,这些命令和EditingCommands类中的一些命令类似。

MediaCommands 该类提供了一组用于处理多媒体的命令(如Play、Pause、NextTrack以及IncreaseVolume)。

这些单独的命令对象仅是一些标志器,不具有实际功能。然而,许多命令都有一个额外的特征:默认输入绑定。例如,ApplicationCommands.Open 命令被映射到Ctrl+O快捷键。只要将命令绑定到命令源,并未窗口添加命令源,这个快捷键就会被激活,即使没有在用户界面的任何地方显示该命令也同样如此。

命令源

命令库中的命令始终可用。触发他们最简单的方法是将他们关联到实现了ICommandSource接口的控件,其中包括继承自ButtonBase类的控件(Button和CheckBox等)、单独的ListBoxItemduix 、Hyperlink 以及MenuItem。

ICommandSource接口定义了三个属性:

    public interface ICommandSource{ICommand Command { get; }object CommandParameter { get; }IInputElement CommandTarget { get; }}

Command 指向连接的命令,这是唯一必需的细节

CommandParameter 提供其他希望随命令发送的数据

CommandTarget 确定将在其中执行命令的元素

命令绑定

将命令关联到命令源时,会看到命令源会被自动禁用。这是因为命令源已经查询到命令的状态,而且由于命令还没有与其关联的绑定,所以命令源被认为是禁用的

<Button Command="ApplicationCommands.New"  Content="New" />

为改变这种状态,需要为命令创建绑定以明确以下三件事情:

1、当命令被触发时执行什么操作

2、如何确定命令是否能够被执行(这是可选的。如果未提供这一细节,只要提供了关联的事件处理程序,命令总是可用的)。

3、命令在何处起作用。例如,命令可被限制在单个按钮中使用,或在整个窗口中使用(这种情况更常见)。

通过代码为命令创建绑定

CommandBinding binding = new CommandBinding(ApplicationCommands.New);
binding.Executed += NewCommandExecuted;
this.CommandBindings.Add(binding);

这里创建的CommandBinding对象被添加到包含窗口的CommandBindings集合中,这通过事件冒泡进行工作。实际上,当单击按钮时,CommandBinding.Executed 事件从按钮冒泡到包含元素。

通过XAML标记创建命令绑定:

<Window.CommandBindings><CommandBinding Command="Open" Executed="OpenCommandExecuted" />
</Window.CommandBindings>

尽管习惯上为窗口添加所有绑定,单CommandBindings属性实际是在 UIElement基类中定义的。这意味着任何元素都支持该属性(例如,如果将命令绑定到使用它的按钮中)。为得到最大的灵活性,命令绑定通常被添加到顶级窗口。如果希望在多个窗口中使用相同的命令,需要在这些窗口中分别创建命令绑定。

也可以处理CommandBinding.PreviewExecuted事件,首先在最高层次的容器中引发该事件,然后隧道路由至按钮。在事件完成前,可通过事件隧道拦截和停止事件。如果将 RoutedEventArgs.Handled 属性设置为true,将永远不会发生Executed事件。

使用多命令源

在按钮中触发事件看起不那么直接,然而,当添加相同的命令的更多控件时,额外命令层的意义就会体现出来。

<Menu><MenuItem Header="File"><MenuItem Command="New" /></MenuItem>
</Menu>

这里并没有为MenuItem设置Header属性。这是因为MenuItem类足够智能,如果没有设置Header属性,它将从命令中提取文件(Button控件不具有这一特性)。虽然该特性带来的便利看起来不大,但如果计划使用不同的语言本地化应用程序,这一特性就很重要了。在这一情况下,只需要在一个地方修改文本即可(通过设置命令的Text属性),这比在整个窗口中进行跟踪更容易。

MenuItem类还有一项功能:能自动提取 Command.InputBindings 集合中的第一个快捷键(如果存在的话)。对于ApplicationCommands.New 命令对象,这意味着在菜单文本旁边会显示Ctrl+N 快捷键。

需要注意的是,不需要为菜单项另外创建命令绑定。前面在窗口下创建的命令绑定现在被两个不同的控件使用,每个控件都将它们的工作传递给同一个命令事件处理程序。

微调命令文本

前面看到菜单具有自动提取命令的文本的功能,它实际获取的 RoutedUICommand 的 Text属性,那么其他的实现ICommandSource的控件应该也能提取相应Command的Text属性。我们可以通过数据绑定来获取:

<Button Command="ApplicationCommands.New"  Content="{Binding RelativeSource={RelativeSource Mode=Self}, Path=Command.Text}" />

直接调用命令

并非只能使用实现了ICommandSource 接口的类来触发希望执行的命令。也可以用Execute() 方法直接调用来自任何事件处理程序的方法。这是需传递参数值(或null引用)和对目标元素的引用。

ApplicationCommands.New.Execute(null, target);

也可以在关联的CommandBinding对象中调用Execute()方法。在这种情况下,不需要提供目标元素,因为会自动将公开正在使用CommandBindings集合的元素设置为目标元素

this.CommandBindings[0].Command.Execute(null);

这种方法只是用了半个命令模型,虽然也可触发命令,但不能响应命令的状态变化。

禁用命令

如果想要创建状态在启用和禁用之间变化的命令,您将体会到命令模型的真正优势。命令的启用与禁用通过CommandBinding的 CanExecute 事件控制,可通过如下方式为该事件关联事件处理程序:

<CommandBinding Command="Save" Executed="SaveCommand_Executed" CanExecute="SaveCommand_CanExecute"/>

在事件处理程序中,只需相应的设置 CanExecuteRoutedEventArgs.CanExecute 属性

private void SaveCommand_CanExecute(object sender, CanExecuteRoutedEventArgs e)
{e.CanExecute = isDirty;
}

在这里,当isDirty为false时,就禁用命令;当isDirty为true时,启用命令。

当使用 CanExecute 事件时,还需要理解一个问题。由WPF负责调用 RoutedCommandCanExecute () 方法来触发事件处理程序,并确定命令的状态。当WPF命令管理器探测到某个确信十分重要的变化时——例如,当焦点从一个控件移到另一控件上时,或执行了某个命令后,WPF命令管理器就会完成该工作。控件还能引发 CanExecuteChanged 事件,以通知WPF重新评估命令——例如,当用户在文本框中按下一个键时会发生该事件。总之,CanExecute事件会被频繁地触发,不应该在该事件处理程序中使用耗时的代码。

具有内置命令的控件

一些输入控件可以自行处理命令事件。例如,TextBox类处理Cut、Copy、Paste命令等。当控件具有自己的硬编码命令逻辑时,为使命令工作不需要做其他任何事情。例如,对于TextBox控件,添加以下工具栏按钮,就会自动获得对剪切、复制、和粘贴文本的支持

<ToolBar><Button Command="Cut" Content="{Binding RelativeSource={RelativeSource Mode=Self}, Path=Command.Text}"/><Button Command="Copy" Content="{Binding RelativeSource={RelativeSource Mode=Self}, Path=Command.Text}"/><Button Command="Paste" Content="{Binding RelativeSource={RelativeSource Mode=Self}, Path=Command.Text}"/>
</ToolBar>

现在单击这些按钮中的任意一个(当文本框具有焦点时),就可以复制、剪切或从剪贴板粘贴文本。有趣的是,文本框还处理CanExecute事件。如果当前未在文本框中选中任何内容,就会禁用剪切和复制命令。当焦点移动到其它不支持这些命令的控件时,会自动禁用所有者三个命令(除非关联自己的CanExecute事件处理程序以启用这些命令)。

这里有一个有趣的细节。Cut、Copy、Paste命令被具有焦点的文本框处理。然而,有工具栏上的按钮触发的命令是完全独立的元素。这个过程之所以能够无缝工作,是因为按钮被放到工具栏上,ToolBar提供了一些内置逻辑,可将其子元素的CommandTarget属性动态设置为当前具有焦点的控件(从技术角度看,ToolBar控件一直在关注着其父元素,及窗口,并在上下文中查找最近具有焦点的控件,即文本框。ToolBar控件有单独的焦点范围focus scope,并且在其上下文中按钮时具有焦点的)。

如果是在不同容器(不是ToolBar或Menu控件)中放置按钮,就不会获得这项优势。这意味着除非手动设置CommandTarget 属性,否则按钮不能工作。为此,必须使用命名目标元素的绑定表达式。例如,如果文本框被命名为 txt,就应该向下面这样定义按钮:

<Button Command="Cut" CommandTarget="{Binding ElementName=txt}" Content="{Binding RelativeSource={RelativeSource Mode=Self}, Path=Command.Text}"/>

另一个较简单的选择是使用附加属性FocusManager.IsFocusScope 创建新的焦点范围。当触发命令时,改焦点范围会通知WPF在父元素范围内查找元素:

<StackPanel><StackPanel Orientation="Horizontal" FocusManager.IsFocusScope="True"><Button Command="Cut" Content="{Binding RelativeSource={RelativeSource Mode=Self}, Path=Command.Text}"/><Button Command="Copy" Content="{Binding RelativeSource={RelativeSource Mode=Self}, Path=Command.Text}"/><Button Command="Paste" Content="{Binding RelativeSource={RelativeSource Mode=Self}, Path=Command.Text}"/></StackPanel><TextBox TextWrapping="Wrap" AcceptsReturn="True"></TextBox>
</StackPanel>

该方法还有一个附加优点,即相同的命令可应用于多个控件,不像上面那样对CommandTarget进行硬编码。此外,Menu和ToolBar 控件默认将FocusManager.IsFocusScope属性设置为true,但如果希望简化命令路由行为,不在父元素上下文中查找具有焦点的元素,也可以将该属性设为false。

在极少数情况下,控件支持内置命令,但并不需要,这种情况下,可以采用三种方式禁用命令:

1、理想情况下,控件会提供用于关闭命令支持的属性,从而确保控件移除这些特性并连贯地调整自身。例如,TextBox控件提供了IsUndoEnabled 属性,为阻止Undo特性,可将该属性设置为false(如果IsUndoEnabled属性为true,Ctrl+Z组合键将触发Undo命令)。

2、可为希望禁用的命令添加新的命令绑定。然后该命令绑定可提供新的 CanExecute 事件处理程序,并总是响应 false,并注意还要设置Handled 标志以阻止文本框自我执行计算,而文本框可能将CanExecute 属性设置为true。

3、使用InputBinding集合删除触发命令的输入。

<TextBox IsUndoEnabled="False" Text="Hello"><TextBox.CommandBindings><CommandBinding Command="Cut" CanExecute="CommandBinding_CanExecute"/></TextBox.CommandBindings><TextBox.InputBindings><KeyBinding Command="NotACommand" Key="C" Modifiers="Ctrl"/></TextBox.InputBindings>
</TextBox>
    private void CommandBinding_CanExecute(object sender, CanExecuteRoutedEventArgs e){e.CanExecute = false;e.Handled = true;}

这样一个文本框将会阻止撤销、剪切以及Ctrl+C键的复制。

自定义命令

RoutedUICommand 类提供了几个构造函数。虽然可创建没有任何附加信息的RoutedUICommand对象,但几乎总是希望提供命令名、命令文本以及所属类型。此外,可能希望为InputGestures 集合提供快捷键。

最佳设计是遵循WPF库中的范例,并通过静态属性提供自定义命令。

public class CustomCommands
{private static RoutedUICommand requery;public static RoutedUICommand Requery { get => requery; }static CustomCommands(){InputGestureCollection inputs = new InputGestureCollection();inputs.Add(new KeyGesture(Key.R, ModifierKeys.Control, "Ctrl+R"));requery = new RoutedUICommand("Requery", "Requery", typeof(CustomCommands), inputs);}
}

一旦定义了命令,就可以在命令绑定中使用它,就像使用WPF提供的所有预先构建好的命令那样。如果希望在XAML中使用自定义命令,注意需要带上名称空间映射。

<StackPanel><StackPanel.CommandBindings><CommandBinding Command="local:DataCommands.Requery" Executed="CommandBinding_Executed"/></StackPanel.CommandBindings><Button Command="local:DataCommands.Requery" Content="Requery"/>
</StackPanel>

在不同的位置使用相同的命令

在WPF命令模型中,一个重要概念是范围。尽管每个命令仅有一份副本,但使用命令的效果却会根据触发命令的位置而异。例如,有两个文本框,它们都支持Cut、Copy和Paste命令,操作只会在当前具有焦点的文本框中发生,但是对于需要自己实现的命令——New、Open、Save情况就不同了。问题在于当为这些命令的某个命令触发Executed事件时,不知道该事件是属于第一个文本框还是第二个文本框。尽管ExecuteRoutedEventArgs对象提供了Source属性,但该属性反映的是具有命令绑定的元素(像sender引用);而所有命令都被绑定到容器级。

解决这个问题的方法是使用文本框的CommandBindings集合分别为每个文本框绑定命令。这里需要为两个TextBox创建两个相同的CommandBindings,实际上只需要一个。可以向两个TextBox添加同一个命令绑定,如果使用XAML,需要将命令绑定添加到资源中,然后在TextBox的CommandBindings集合中使用StaticResource标记扩展并提供键名。

<StackPanel><StackPanel.Resources><CommandBinding x:Key="saveCommandBinding" Command="Save" Executed="TwoTextBox_SaveCommand_Executed" CanExecute="TwoTextBox_SaveCommand_CanExecute"/></StackPanel.Resources><ToolBarTray ><ToolBar><Button Command="Save" Content="{Binding RelativeSource={RelativeSource Mode=Self}, Path=Command.Text}"/></ToolBar></ToolBarTray><TextBox TextWrapping="Wrap" AcceptsReturn="True" TextChanged="TwoTextBox_TextChanged"><TextBox.CommandBindings><StaticResource ResourceKey="saveCommandBinding"/></TextBox.CommandBindings></TextBox><TextBox TextWrapping="Wrap" AcceptsReturn="True" TextChanged="TwoTextBox_TextChanged"><TextBox.CommandBindings><StaticResource ResourceKey="saveCommandBinding"/></TextBox.CommandBindings></TextBox>
</StackPanel>

使用命令参数

有些命令需要一些额外信息,例如,NavigationCommands.Zoom命令需要用于缩放的百分数,或者前面说到的两个文本框使用Save命令时,需要知道使用的是哪个文件。

解决方法是设置CommandParameter 属性。可直接为 ICommandSource 控件设置该属性。

<StackPanel><StackPanel.Resources><local:FontStringValueConverter x:Key="StringConverterResource"/></StackPanel.Resources><TextBox Name="txtValue" Background="AliceBlue" Margin="5">5</TextBox><Button Command="{x:Static local:DataCommands.Requery}" CommandTarget="{Binding ElementName=txtBoxTarget}"CommandParameter="{Binding ElementName=txtValue, Path=Text, Converter={StaticResource StringConverterResource}}" Content="UpdateFontSize"/> <TextBox Name="txtBoxTarget" Height="100" Margin="3"><TextBox.CommandBindings><CommandBinding Command="{x:Static local:DataCommands.Requery}" Executed="FontSizeCommandBinding_Executed"/></TextBox.CommandBindings>Hello</TextBox>
</StackPanel>
    private void FontSizeCommandBinding_Executed(object sender, ExecutedRoutedEventArgs e){TextBox? source = sender as TextBox;if(source != null){if(e.Parameter != null){try{if ((int)e.Parameter > 0 && (int)e.Parameter < 60){source.FontSize = (int)e.Parameter;}}catch{MessageBox.Show("in Command \n Parameter: " + e.Parameter);}}}}

跟踪和翻转命令

WPF命令模型缺少的一个特性是翻转命令。尽管提供了ApplicationCommands.Undo命令,但该命令通常用于编辑控件(如TextBox)以维护他们自己的Undo历史。如果希望支持应用程序范围内的Undo特性,需要在内部跟踪以前的状态。

遗憾的是,扩展WPF命令系统并不容易。相对来说没几个入口点可用于连接自定义逻辑,并且对于可用的几个入口点也没有提供说明文档。为创建通用的、可重用的Undo特性,需要创建一组全新的“能够撤销的”命令类,以及一个特定的命令绑定。本质上,必须使用自己创建的新命令系统替换WPF命令系统。

更好的解决方案是设计自己的用于跟踪和翻转命令的系统,但使用CommandManager类保存命令历史。

第一个细节是用于跟踪命令历史的类。为构建保存最近命令历史的撤销系统,可能需要用到这样的类(比如创建派生的ReversibleCommand类,提供诸如Unexecute的方法来翻转以前的任务)。但该系统不能工作,因为所有WPF命令都是唯一的。这意味着在应用程序中每个命令只有一个实例。

为理解该问题,假设提供EditingCommands.Backspace 命令,而且用户在一行中回退了几个空格。可通过向最近命令堆栈中添加Backspace命令来记录这一操作,但实际上每次添加的是相同的命令对象。因此,没有简单的方法用于存储命令的其它信息,例如刚刚删除的字符。如果希望存储该状态,需要构建自己的数据结构。该结构跟踪以下几部分信息:

1、命令名称。

2、执行命令的元素。在这里有两个文本框,所以可以是其中任意一个

3、在目标元素中被改变了的属性。在这里是TextBox类的Text属性

4、可用于保存受影响元素以前状态的对象。

CommandHistoryItem类还提供了通用的Undo方法,该方法使用反射为修改过的属性应用以前的值,用于恢复TextBox控件中的文本。但对于更复杂的应用程序,需要使用CommandHistoryItem 类的层次结构,每个类都可以使用不同方式翻转不同类型的操作。

public class CommandHistoryItem
{public string CommandName { get; set; }public UIElement? ElementActedOn { get; set; }public string PropertyActedOn { get; set; }public object? PreviousState { get; set; }public CommandHistoryItem(string commandName): this(commandName, null, "", null){}public CommandHistoryItem(string commandName, UIElement? elementActedOn, string propertyActedOn, object? previousState){CommandName = commandName;ElementActedOn = elementActedOn;PropertyActedOn = propertyActedOn;PreviousState = previousState;}public bool CanUndo{get { return (ElementActedOn != null && PropertyActedOn != ""); }}public void Undo(){Type? elementType = ElementActedOn?.GetType();PropertyInfo? property = elementType?.GetProperty(PropertyActedOn);property?.SetValue(ElementActedOn, PreviousState, null);}
}

这一设计非常巧妙,可以为元素存储状态。如果存储整个窗口状态的快照,那么会显著增加内存的使用量。然而,如果有大量数据(比如文本框有几十行文本),Undo操作的负担就很大了。解决方法是限制在历史中存储的项的数量,或使用更加智能的方法只存储被改变的数据的信息,而不是存储所有数据。

需要的下一个操作要素是执行应用程序范围内Undo操作的命令。ApplicationCommands.Undo命令是不合适的,原因是为了达到不同的目的,它已经被用于单独的文本框控件(翻转最后的编辑变化)。相反,需要创建一个新命令:CustomCommands.ApplicationUndo

public class CustomCommands
{private static RoutedUICommand requery;public static RoutedUICommand Requery { get => requery; }private static RoutedUICommand applicationUndo;public static RoutedUICommand ApplicationUndo { get=> applicationUndo; }static CustomCommands(){InputGestureCollection inputs = new InputGestureCollection();inputs.Add(new KeyGesture(Key.R, ModifierKeys.Control, "Ctrl+R"));requery = new RoutedUICommand("Requery", "Requery", typeof(CustomCommands), inputs);applicationUndo = new RoutedUICommand("ApplicationUndo", "Application Undo", typeof(CustomCommands));}
}

响应特定命令非常简单,但当执行任何命令时,如何进行响应并记录呢?技巧是使用CommandManager类,该类提供了几个静态事件:

        public static readonly RoutedEvent CanExecuteEvent;public static readonly RoutedEvent ExecutedEvent;public static readonly RoutedEvent PreviewCanExecuteEvent;public static readonly RoutedEvent PreviewExecutedEvent;

这里我们主要关注ExecutedEvent 和 PreviewExecutedEvent 事件,因为每当执行任何一个命令时都会引发它们。

尽管CommandManager类挂起了Executed事件,但仍可使用UIElement.AddHandler()方法关联事件处理程序,并未可选的第三个参数传递true值。这样将允许接收事件,即使事件已经被处理过也同样如此。然而,Executed事件是在命令执行完成之后被触发的,这时已经来不及在命令历史中保存被影响的控件的状态了。相反,需要响应PreviewExecuted事件,该事件在命令执行前一刻被触发。

public partial class MainWindow : Window
{public MainWindow(){InitializeComponent();AddCommandBinding();this.AddHandler(CommandManager.PreviewExecutedEvent, new ExecutedRoutedEventHandler(CommandExecuted));}private void NewCommandExecuted(object sender, ExecutedRoutedEventArgs e){MessageBox.Show("New command triggered by " + e.Source.ToString());if(e.Source != null){var target = e.Source as Control;if (target != null){if (target.Background == Brushes.Blue){target.Background = Brushes.Red;}else{target.Background = Brushes.Blue;}}}}private void ApplicationUndoCommand_Executed(object sender, ExecutedRoutedEventArgs e){CommandHistoryItem historyItem = (CommandHistoryItem)lstHistory.Items[^1];if (historyItem.CanUndo)historyItem.Undo();lstHistory.Items.Remove(historyItem);}private void ApplicationUndoCommand_CanExecute(object sender, CanExecuteRoutedEventArgs e){if (lstHistory == null || lstHistory.Items.Count == 0)e.CanExecute = false;elsee.CanExecute = true;}
}
<StackPanel><StackPanel.CommandBindings><CommandBinding Command="local:CustomCommands.ApplicationUndo" Executed="ApplicationUndoCommand_Executed" CanExecute="ApplicationUndoCommand_CanExecute"></CommandBinding></StackPanel.CommandBindings><ToolBarTray><ToolBar><Button Command="ApplicationCommands.Cut">Cut</Button><Button Command="ApplicationCommands.Copy">Copy</Button><Button Command="ApplicationCommands.Paste">Paste</Button><Button Command="ApplicationCommands.Undo">Undo</Button></ToolBar><ToolBar><Button Command="local:CustomCommands.ApplicationUndo">ReverseLastCommand</Button></ToolBar></ToolBarTray><TextBox Margin="5" TextWrapping="Wrap" AcceptsReturn="True"/><TextBox Margin="5" TextWrapping="Wrap" AcceptsReturn="True"/><ListBox Grid.Row="3" Name="lstHistory" Margin="5" DisplayMemberPath="CommandName"></ListBox>
</StackPanel>

要在实际应用程序中使用这一方法,还需要进行许多改进。例如,需要耗费大量时间改进 CommandManager.PreviewExecutedEvent 事件处理程序,以忽略那些明显不需要跟踪的命令(诸如使用键盘选择文本的事件、单击空格键引发的命令等)。类似地,可能希望为那些不是由命令表示的但应当被翻转的操作添加CommandHistoryItem 对象。例如,输入一些文本,然后导航到其它控件。最后,可能希望将Undo历史限制在最近执行的命令范围之内。

测试代码展示

MainWindow.xaml

<Window x:Class="TestCommand.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:TestCommand"mc:Ignorable="d"Title="MainWindow" Height="450" Width="800"><StackPanel Name="stackPanel"><StackPanel Name="stackPanel1"><StackPanel.CommandBindings><CommandBinding Command="Open" Executed="OpenCommandExecuted" /></StackPanel.CommandBindings><Menu FocusManager.IsFocusScope="False"><MenuItem Header="File"><MenuItem Command="New" /></MenuItem></Menu><Button Command="ApplicationCommands.New"  Content="{Binding RelativeSource={RelativeSource Mode=Self}, Path=Command.Text}" /><Button Click="cmdDoCommand_Click">DoCommand</Button></StackPanel><StackPanel><StackPanel.CommandBindings><CommandBinding Command="New" Executed="NewCommand_Executed"/><CommandBinding Command="Open" Executed="OpenCommand_Executed"/><CommandBinding Command="Save" Executed="SaveCommand_Executed" CanExecute="SaveCommand_CanExecute"/></StackPanel.CommandBindings><Menu><MenuItem Header="File"><MenuItem Command="New"></MenuItem><MenuItem Command="Open"></MenuItem><MenuItem Command="Save"></MenuItem><MenuItem Command="SaveAs"></MenuItem><Separator></Separator><MenuItem Command="Close"></MenuItem></MenuItem></Menu><ToolBarTray ><ToolBar><Button Command="New" Content="{Binding RelativeSource={RelativeSource Mode=Self}, Path=Command.Text}"/><Button Command="Open" Content="{Binding RelativeSource={RelativeSource Mode=Self}, Path=Command.Text}"/><Button Command="Save" Content="{Binding RelativeSource={RelativeSource Mode=Self}, Path=Command.Text}"/> </ToolBar><ToolBar><Button Command="Cut" Content="{Binding RelativeSource={RelativeSource Mode=Self}, Path=Command.Text}"/><Button Command="Copy" Content="{Binding RelativeSource={RelativeSource Mode=Self}, Path=Command.Text}"/><Button Command="Paste" Content="{Binding RelativeSource={RelativeSource Mode=Self}, Path=Command.Text}"/></ToolBar></ToolBarTray><TextBox TextWrapping="Wrap" AcceptsReturn="True" TextChanged="TextBox_TextChanged"></TextBox></StackPanel><StackPanel><StackPanel Orientation="Horizontal"><Button Command="Cut" CommandTarget="{Binding ElementName=txt}" Content="{Binding RelativeSource={RelativeSource Mode=Self}, Path=Command.Text}"/><Button Command="Copy" Content="{Binding RelativeSource={RelativeSource Mode=Self}, Path=Command.Text}"/><Button Command="Paste" Content="{Binding RelativeSource={RelativeSource Mode=Self}, Path=Command.Text}"/></StackPanel><TextBox Name="txt" TextWrapping="Wrap" AcceptsReturn="True"></TextBox></StackPanel><StackPanel><StackPanel Orientation="Horizontal" FocusManager.IsFocusScope="True"><Button Command="Cut" Content="{Binding RelativeSource={RelativeSource Mode=Self}, Path=Command.Text}"/><Button Command="Copy" Content="{Binding RelativeSource={RelativeSource Mode=Self}, Path=Command.Text}"/><Button Command="Paste" Content="{Binding RelativeSource={RelativeSource Mode=Self}, Path=Command.Text}"/></StackPanel><TextBox TextWrapping="Wrap" AcceptsReturn="True"></TextBox></StackPanel><StackPanel ><TextBox IsUndoEnabled="False" Text="Hello"><TextBox.CommandBindings><CommandBinding Command="Cut" CanExecute="CommandBinding_CanExecute"/></TextBox.CommandBindings><TextBox.InputBindings><KeyBinding Command="NotACommand" Key="C" Modifiers="Ctrl"/></TextBox.InputBindings></TextBox></StackPanel><StackPanel><StackPanel.CommandBindings><CommandBinding Command="local:CustomCommands.Requery" Executed="CommandBinding_Executed"/></StackPanel.CommandBindings><Button Command="local:CustomCommands.Requery" Content="Requery"/></StackPanel><StackPanel><StackPanel.Resources><CommandBinding x:Key="saveCommandBinding" Command="Save" Executed="TwoTextBox_SaveCommand_Executed" CanExecute="TwoTextBox_SaveCommand_CanExecute"/></StackPanel.Resources><ToolBarTray ><ToolBar><Button Command="Save" Content="{Binding RelativeSource={RelativeSource Mode=Self}, Path=Command.Text}"/></ToolBar></ToolBarTray><TextBox TextWrapping="Wrap" AcceptsReturn="True" TextChanged="TwoTextBox_TextChanged"><TextBox.CommandBindings><StaticResource ResourceKey="saveCommandBinding"/></TextBox.CommandBindings></TextBox><TextBox TextWrapping="Wrap" AcceptsReturn="True" TextChanged="TwoTextBox_TextChanged"><TextBox.CommandBindings><StaticResource ResourceKey="saveCommandBinding"/></TextBox.CommandBindings></TextBox></StackPanel><StackPanel><StackPanel.Resources><local:FontStringValueConverter x:Key="StringConverterResource"/></StackPanel.Resources><TextBox Name="txtValue" Background="AliceBlue" Margin="5">5</TextBox><Button Command="{x:Static local:CustomCommands.Requery}" CommandTarget="{Binding ElementName=txtBoxTarget}"CommandParameter="{Binding ElementName=txtValue, Path=Text, Converter={StaticResource StringConverterResource}}" Content="UpdateFontSize"/> <TextBox Name="txtBoxTarget" Height="100" Margin="3"><TextBox.CommandBindings><CommandBinding Command="{x:Static local:CustomCommands.Requery}" Executed="FontSizeCommandBinding_Executed"/></TextBox.CommandBindings>Hello</TextBox></StackPanel><StackPanel><StackPanel.CommandBindings><CommandBinding Command="local:CustomCommands.ApplicationUndo" Executed="ApplicationUndoCommand_Executed" CanExecute="ApplicationUndoCommand_CanExecute"></CommandBinding></StackPanel.CommandBindings><ToolBarTray><ToolBar><Button Command="ApplicationCommands.Cut">Cut</Button><Button Command="ApplicationCommands.Copy">Copy</Button><Button Command="ApplicationCommands.Paste">Paste</Button><Button Command="ApplicationCommands.Undo">Undo</Button></ToolBar><ToolBar><Button Command="local:CustomCommands.ApplicationUndo">ReverseLastCommand</Button></ToolBar></ToolBarTray><TextBox Margin="5" TextWrapping="Wrap" AcceptsReturn="True"/><TextBox Margin="5" TextWrapping="Wrap" AcceptsReturn="True"/><ListBox Grid.Row="3" Name="lstHistory" Margin="5" DisplayMemberPath="CommandName"></ListBox></StackPanel></StackPanel>
</Window>

MainWindow.xaml.cs

using System;
using System.Collections.Generic;
using System.Globalization;
using System.Linq;
using System.Reflection;
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 TestCommand;[ValueConversion(typeof(string), typeof(int))]
public class FontStringValueConverter : IValueConverter
{public object Convert(object value, Type targetType, object parameter, CultureInfo culture){string fontSize = (string)value;int intFont;try{intFont = int.Parse(fontSize);return intFont;}catch{return 0;}}public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture){return 0;}
}
public class CustomCommands
{private static RoutedUICommand requery;public static RoutedUICommand Requery { get => requery; }private static RoutedUICommand applicationUndo;public static RoutedUICommand ApplicationUndo { get=> applicationUndo; }static CustomCommands(){InputGestureCollection inputs = new InputGestureCollection();inputs.Add(new KeyGesture(Key.R, ModifierKeys.Control, "Ctrl+R"));requery = new RoutedUICommand("Requery", "Requery", typeof(CustomCommands), inputs);applicationUndo = new RoutedUICommand("ApplicationUndo", "Application Undo", typeof(CustomCommands));}
}public class CommandHistoryItem
{public string CommandName { get; set; }public UIElement? ElementActedOn { get; set; }public string PropertyActedOn { get; set; }public object? PreviousState { get; set; }public CommandHistoryItem(string commandName): this(commandName, null, "", null){}public CommandHistoryItem(string commandName, UIElement? elementActedOn, string propertyActedOn, object? previousState){CommandName = commandName;ElementActedOn = elementActedOn;PropertyActedOn = propertyActedOn;PreviousState = previousState;}public bool CanUndo{get { return (ElementActedOn != null && PropertyActedOn != ""); }}public void Undo(){Type? elementType = ElementActedOn?.GetType();PropertyInfo? property = elementType?.GetProperty(PropertyActedOn);property?.SetValue(ElementActedOn, PreviousState, null);}
}public partial class MainWindow : Window
{public MainWindow(){InitializeComponent();AddCommandBinding();this.AddHandler(CommandManager.PreviewExecutedEvent, new ExecutedRoutedEventHandler(CommandExecuted));}private void AddCommandBinding(){CommandBinding binding = new CommandBinding(ApplicationCommands.New);binding.Executed += NewCommandExecuted;stackPanel1.CommandBindings.Add(binding);}private void NewCommandExecuted(object sender, ExecutedRoutedEventArgs e){MessageBox.Show("New command triggered by " + e.Source.ToString());if(e.Source != null){var target = e.Source as Control;if (target != null){if (target.Background == Brushes.Blue){target.Background = Brushes.Red;}else{target.Background = Brushes.Blue;}}}}private void OpenCommandExecuted(object sender, ExecutedRoutedEventArgs e){MessageBox.Show("Open command triggered by " + e.Source.ToString());}private void cmdDoCommand_Click(object sender, RoutedEventArgs e){ApplicationCommands.New.Execute(null, null);//stackPanel1.CommandBindings[0].Command.Execute(null);}private bool isDirty = false;private void NewCommand_Executed(object sender, ExecutedRoutedEventArgs e){MessageBox.Show("New command triggered with " + e.Source.ToString());isDirty = false;}private void OpenCommand_Executed(object sender, ExecutedRoutedEventArgs e){isDirty = false;}private void SaveCommand_Executed(object sender, ExecutedRoutedEventArgs e){MessageBox.Show("Save command triggered with " + e.Source.ToString());isDirty = false;}private void SaveCommand_CanExecute(object sender, CanExecuteRoutedEventArgs e){e.CanExecute = isDirty;}private void TextBox_TextChanged(object sender, TextChangedEventArgs e){isDirty = true;}private void CommandBinding_CanExecute(object sender, CanExecuteRoutedEventArgs e){e.CanExecute = false;e.Handled = true;}private void CommandBinding_Executed(object sender, ExecutedRoutedEventArgs e){MessageBox.Show("Command triggered by " + e.Source.ToString());}private Dictionary<Object, bool> isDirtys = new Dictionary<Object, bool>();private void TwoTextBox_TextChanged(object sender, TextChangedEventArgs e){isDirtys[sender] = true;}private void TwoTextBox_SaveCommand_Executed(object sender, ExecutedRoutedEventArgs e){string text = ((TextBox)sender).Text;MessageBox.Show("About to save: " + text);isDirtys[sender] = false;}private void TwoTextBox_SaveCommand_CanExecute(object sender, CanExecuteRoutedEventArgs e){if (isDirtys.ContainsKey(sender) && isDirtys[sender] == true){e.CanExecute = true;}else{e.CanExecute = false;}}private void FontSizeCommandBinding_Executed(object sender, ExecutedRoutedEventArgs e){TextBox? source = sender as TextBox;if(source != null){if(e.Parameter != null){try{if ((int)e.Parameter > 0 && (int)e.Parameter < 60){source.FontSize = (int)e.Parameter;}}catch{MessageBox.Show("in Command \n Parameter: " + e.Parameter);}}}}private void CommandExecuted(object sender, ExecutedRoutedEventArgs e){// Ignore menu button source.if (e.Source is ICommandSource)return;// Ignore the ApplicationUndo command.if (e.Command == CustomCommands.ApplicationUndo)return;// Could filter for commands you want to add to the stack// (for example, not selection events).TextBox? txt = e.Source as TextBox;if (txt != null){RoutedCommand cmd = (RoutedCommand)e.Command;CommandHistoryItem historyItem = new CommandHistoryItem(cmd.Name, txt, "Text", txt.Text);ListBoxItem item = new ListBoxItem();item.Content = historyItem;lstHistory.Items.Add(historyItem);// CommandManager.InvalidateRequerySuggested();}}private void ApplicationUndoCommand_Executed(object sender, ExecutedRoutedEventArgs e){CommandHistoryItem historyItem = (CommandHistoryItem)lstHistory.Items[^1];if (historyItem.CanUndo)historyItem.Undo();lstHistory.Items.Remove(historyItem);}private void ApplicationUndoCommand_CanExecute(object sender, CanExecuteRoutedEventArgs e){if (lstHistory == null || lstHistory.Items.Count == 0)e.CanExecute = false;elsee.CanExecute = true;}
}

相关文章:

WPF命令

在设计良好的Windows应用程序中&#xff0c;应用程序逻辑不应位于事件处理程序中&#xff0c;而应在更高层的方法中编写代码。其中的每个方法都代表单独的应用程序任务。每个任务可能依赖其他库。 使用这种设计最明显的方式是在需要的地方添加事件处理程序&#xff0c;并使用各…...

Unity中Shader的屏幕抓取 GrabPass

文章目录 前言一、抓取1、抓取指令2、在使用抓取的屏幕前&#xff0c;需要像使用属性一样定义一下,_GrabTexture这个名字是Unity定义好的 前言 Unity中Shader的屏幕抓取 GrabPass 一、抓取 1、抓取指令 屏幕的抓取需要使用一个Pass GrabPass{} GrabPass{“NAME”} 2、在使用…...

手撕 队列

队列的基本概念 只允许在一端进行插入数据操作&#xff0c;在另一端进行删除数据操作的特殊线性表&#xff0c;队列具有先进先出 入队列&#xff1a;进行插入操作的一端称为队尾 出队列&#xff1a;进行删除操作的一端称为队头 队列用链表实现 队列的实现 队列的定义 队列…...

【autodl/linux配环境心得:conda/本地配cuda,cudnn及pytorch心得】-未完成

linux配环境心得&#xff1a;conda/本地配cuda&#xff0c;cudnn及pytorch心得 我们服务器遇到的大多数找不到包的问题一&#xff0c;服务器安装cuda和cudnn使用conda在线安装cuda和cudnn使用conda进行本地安装检查conda安装的cuda和cudnn本地直接安装cuda和cudnn方法一&#x…...

macOS Ventura 13.5.2(22G91)发布,附黑/白苹果镜像下载地址

系统介绍&#xff08;下载请百度搜索&#xff1a;黑果魏叔&#xff09; 黑果魏叔 9 月 8 日消息&#xff0c;苹果今日向 Mac 电脑用户推送了 macOS 13.5.2 更新&#xff08;内部版本号&#xff1a;22G91&#xff09;&#xff0c;本次更新距离上次发布隔了 21 天。 本次更新查…...

vue 子组件向父组件传递参数 子传父

子组件中写&#xff1a; this.$emit(RowCount,res.data.RowCount); 父组件中写&#xff1a; getMFGLRowCount(val){ //父组件中的方法: 接收子组件传过来的参数值赋值给父组件的变量 //this.totalCount val; alert("这…...

自然语言处理学习笔记(八)———— 准确率

目录 1.准确率定义 2.混淆矩阵与TP/FN/FP/TN 3. 精确率 4.召回率 5.F1值 6.中文分词的P、R、F1计算 7.实现 1.准确率定义 准确率是用来衡量一个系统的准确程度的值&#xff0c;可以理解为一系列评测指标。当预测与答案的数量相等时&#xff0c;准确率指的是系统做出正确判…...

Matlab 如何选择窗函数和 FFT 的长度

Matlab 如何选择窗函数和 FFT 的长度 1、常用的四种窗函数 对于实际信号序列&#xff0c;如何选取窗函数呢&#xff1f;一般来说&#xff0c;选择第一旁瓣衰减大&#xff0c;旁瓣峰值衰减快的窗函数有利于緩解截断过程中产生的頻泄漏问题。但具有这两个特性的窗函数&#xff0…...

node.js下载安装环境配置以及快速使用

目录 一、下载 二、安装 三、测试安装是否成功 四、配置环境 五、测试配置环境是否成功 六、安装淘宝镜像 七、快速上手 1、建立一个自己的工作目录 2、下载工作代码 八、各种配置文件匹配问题入坑 九、总结 一、下载 Node.js 中文网 想选择其他版本或者其他系统使用…...

使用栈检查括号的合法性 C 实现

使用栈检查括号的合法性 思路讲解&#xff1a;首先从数组数组0下标开始&#xff0c;如果是左括号直接无脑压入栈&#xff0c;直到出现右括号开始判断合法与否。遇到右括号分两种情况&#xff0c;第一种是空栈的情况&#xff0c;也就是说我们第一个字符就是右括号&#xff0c;那…...

小白备战大厂算法笔试(四)——哈希表

文章目录 哈希表常用操作简单实现冲突与扩容链式地址开放寻址线性探测多次哈希 哈希表 哈希表&#xff0c;又称散列表&#xff0c;其通过建立键 key 与值 value 之间的映射&#xff0c;实现高效的元素查询。具体而言&#xff0c;我们向哈希表输入一个键 key &#xff0c;则可以…...

云原生Kubernetes:pod基础

目录 一、理论 1.pod 2.pod容器分类 3.镜像拉取策略&#xff08;image PullPolicy&#xff09; 二、实验 1.Pod容器的分类 2.镜像拉取策略 三、问题 1.apiVersion 报错 2.pod v1版本资源未注册 3.取行显示指定pod信息 四、总结 一、理论 1.pod (1) 概念 Pod是ku…...

Ansys Zemax | 手机镜头设计 - 第 3 部分:使用 STAR 模块和 ZOS-API 进行 STOP 分析

本文是 3 篇系列文章的一部分&#xff0c;该系列文章将讨论智能手机镜头模组设计的挑战&#xff0c;从概念、设计到制造和结构变形的分析。本文是三部分系列的第三部分。它涵盖了使用 Ansys Zemax OpticStudio Enterprise 版本提供的 STAR 技术对智能手机镜头进行自动的结构、热…...

CSP-J初赛复习大题整理笔记

本篇全是整理&#xff0c;为比赛准备. 在这里插入代码片 #include<cstdio> using namespace std; int n, m; int a[100], b[100];int main() {scanf_s("%d%d", &n, &m);for (int i 1; i < n; i)a[i] b[i] 0;//将两个数组清0&#xff0c;这…...

面试题 ⑤

1、TCP与UDP的区别 UDPTCP是否连接无连接&#xff0c;即刻传输面向连接&#xff0c;三次握手是否可靠不可靠传输&#xff0c;网络波动拥堵也不会减缓传输可靠传输&#xff0c;使用流量控制和拥塞控制连接对象个数支持一对一&#xff0c;一对多&#xff0c;多对一和多对多交互通…...

硅谷课堂1

文章目录 P1 项目概述P2—P12 MybatisPlus知识回顾P8 MybatisPlus实现逻辑删除P9 QueryWrapper使用P14 项目后端模块介绍P15 项目后端环境搭建P50—P53 整合腾讯云对象存储1、整合腾讯2、腾讯云示例3、讲师头像上传-后端代码P54—P60 课堂分类管理1、课堂分类查询2、课程分类导…...

第6节-PhotoShop基础课程-认识选区

文章目录 前言1.认识选区1.选区原理1.普通选区2.高级选区 2.功能用途1.抠图2.修图3.调色 3.关键操作&#xff08;手术与屠宰的区别&#xff09;2.加选&#xff08;shift 是快捷键&#xff09;3.减选&#xff08;Alt是快捷键&#xff09;4.交集&#xff08;2&#xff0c;3合起来…...

SQLServer如何获取客户端IP

SQLServer如何获取客户端IP 很多用户询问如何通过SQLServer获取客户端IP从而定位一些问题&#xff0c;比如链接泄露&#xff0c;其实主要是利用几个相关视图&#xff0c;如下给出一些SQL方便用户排查 当前链接 SELECT CONNECTIONPROPERTY(PROTOCOL_TYPE) AS PROTOCOL_TYPE,CO…...

爬虫数据清洗可视化实战-就业形势分析

基于采集和分析招聘网站的数据的芜湖就业形势的调查研究 一、引言 本报告旨在分析基于大数据的当地就业形势&#xff0c;并提供有关薪资、工作地点、经验要求、学历要求、公司行业、公司福利以及公司类型及规模的详细信息。该分析是通过网络爬虫技术对招聘网站的数据进行采集…...

Python - 队列【queue】task_done()和join()基本使用

一. 前言 task_done()是Python中queue模块提供的方法&#xff0c;用于通知队列管理器&#xff0c;已经处理完了队列中的一个项目。 queue.task_done()是Queue对象的一个方法&#xff0c;它用于通知Queue对象&#xff0c;队列中的某一项已经被处理完毕。通常在使用Queue对象时…...

springboot web 增加不存在的url返回200状态码 vue 打包设置

spring boot项目增加 html web页面访问 1. 首先 application.properties 文件中增加配置&#xff0c;指定静态资源目录&#xff08;包括html的存放&#xff09; spring.resources.static-locationsclasspath:/webapp/,classpath:/webapp/static/ 2. 项目目录 3. 如果有实现 …...

JavaWeb_LeadNews_Day11-KafkaStream实现实时计算文章分数

JavaWeb_LeadNews_Day11-KafkaStream实现实时计算文章分数 KafkaStream概述案例-统计单词个数SpringBoot集成 实时计算文章分值来源Gitee KafkaStream 概述 Kafka Stream: 提供了对存储与Kafka内的数据进行流式处理和分析的功能特点: Kafka Stream提供了一个非常简单而轻量的…...

python tcp server client示例代码

功能&#xff1a; 实现基本的tcp server端、client端&#xff0c;并引入threading, 保证两端任意链接、断链接&#xff0c;保证两端的稳定运行 IP说明&#xff1a; server不输入IP&#xff0c;默认为本机的IP&#xff0c;client需要输入要链接的server端的IP 端口说明&#x…...

typecho 反序列化漏洞复现

环境搭建 下载typecho14.10.10 https://github.com/typecho/typecho/tags 安装&#xff0c;这里需要安装数据库 PHPINFO POC.php <?php class Typecho_Feed { const RSS1 RSS 1.0; const RSS2 RSS 2.0; const ATOM1 ATOM 1.0; const DATE_RFC822 r; const DATE_W3…...

Python实现SSA智能麻雀搜索算法优化LightGBM分类模型(LGBMClassifier算法)项目实战

说明&#xff1a;这是一个机器学习实战项目&#xff08;附带数据代码文档视频讲解&#xff09;&#xff0c;如需数据代码文档视频讲解可以直接到文章最后获取。 1.项目背景 麻雀搜索算法(Sparrow Search Algorithm, SSA)是一种新型的群智能优化算法&#xff0c;在2020年提出&a…...

Java多线程4种拒绝策略

文章目录 一、简介二、AbortPolicy拒绝策略A. 概述B. 拒绝策略实现原理C. 应用场景D. 使用示例 三、CallerRunsPolicy拒绝策略A. 概述B. 拒绝策略实现原理C. 应用场景D. 使用示例 四、DiscardPolicy拒绝策略A. 概述B. 拒绝策略实现原理C. 应用场景D. 使用示例 五、DiscardOldes…...

MySQL的MHA

1&#xff0e;什么是 MHA MHA&#xff08;MasterHigh Availability&#xff09;是一套优秀的MySQL高可用环境下故障切换和主从复制的软件。 MHA 的出现就是解决MySQL 单点的问题。 MySQL故障切换过程中&#xff0c;MHA能做到0-30秒内自动完成故障切换操作。 MHA能在故障切换的过…...

Java实现链表

在Java中&#xff0c;可以使用类来定义链表的节点&#xff0c;并使用引用数据类型&#xff08;即类名&#xff09;来模拟指针进而构建链表。下面是一个简单的示例。 首先&#xff0c;创建一个节点类 Node&#xff0c;它包含一个值和指向下一个节点的引用&#xff1a; public …...

SpringCloud Alibaba(2021.0.1版本)微服务-OpenFeign以及相关组件使用(保姆级教程)

&#x1f4bb;目录 前言一、简绍二、代码实现1、搭建服务模块1.1、建立父包1.2、建立两个子包&#xff08;service-order、service-product&#xff09;1.3、添加util 工具类 2、添加maven依赖和yml配置文件2.1、springcloud-test父包配置2.2、服务模块配置2.2.1、service-orde…...

豆制品废水处理设备源头厂家方案

豆制品废水处理设备源头厂家方案 豆制品生产过程中产生的废水含有有机物、悬浮物、油脂等污染物&#xff0c;需要经过合理的处理才能达到排放标准或循环再利用。以下是一个可能的豆制品废水处理设备及方案&#xff1a; 1.初步处理&#xff1a; 格栅&#xff1a;用于去除大颗粒的…...