2024年7月

最近工作中要使用
PAG
替换
Lottie
,为了方便阅读源码,使用
XCode
对其源码进行了编译。

1 下载源码

编译源码首先要下载源码,有关
PAG
源码可直接到
github
上下载。

2 添加相关依赖

下载源码之后,进入到
PAG
项目根目录,执行如下脚本:

./sync_deps.sh

3 构建 iOS PAGViewer 工程

PAG
项目包含多种平台下的代码,包括
iOS

Windows

Linux
等。我们只关注
iOS

从根目录进入到
iOS
目录,双击
gen_ios
文件。执行完毕后,目录下会多出一个
PAGViewer.xcworkspace

4 设置 XCode 签名证书

点击
PAGViewer.xcworkspace
,进入到
PAG
项目工程,做下图配置,这样可以将
PAGViewer
编译到自己手机上:

image

当设置完
Team
之后会报错,可以删除
Bundle Identifier
,然后点击
Signing Certification
下面的
Try Again
按钮。

5 编译


XCode
中选择
PAGViewer``Target
以及要
iPhone
手机,点击进行编译:

image

6 脚本设置

能一帆风顺的源码编译经常是可遇而不可求,
PAG
源码编译也一样。

编译过程中,会收到如下报错:

image

看报错信息,是在编译
tgfx
项目,运行脚本时,脚本命令出错,出错的命令是
node
命令。

首先查看
Mac
上是否安装了
node


Mac
终端运行
node -v
,如果输出如下版本信息,说明已经安装:

v22.5.1

既然安装了
node
,那
XCode
为啥还报错找不到呢?

原因是
XCode
运行脚本时的
PATH
环境变量与终端中的
PATH
环境变量不同。

XCode
的进程环境变量是由
macOS
系统在启动时设定的,而不是由用户的
Shell
配置文件(如
~/.bash_profile
)直接设定的。

也就是说,我们安装的
node
的路径,不在
XCode

PATH
环境变量下。

为了查看
XCode

PATH
环境变量,可以按照下图进行设置,将
XCode

PATH
环境变量输出到
XCode

Report
面板:

image

上图中
CMake PostBuild Rules
会执行一个脚本,正是这个脚本在编译中报错。

在脚本中添加
echo
命令,输出
XCode

PATH
信息:

echo "当前 PATH: $PATH"

再次编译
PAGViewer
Target
,仍然失败,但是
XCode

Report
面板已有输出信息:

image

将上面
XCode

PATH
信息拷贝保存下来,然后在
Mac
终端运行
which node
命令,查看
Mac

node
的安装目录:

localhost:~$ which node
/opt/homebrew/bin/node

可以验证,
Mac
上的
node
安装目录
/opt/homebrew/bin
不在
XCode

PATH
环境变量中。

为了解决这个问题,我们需要将这个目录添加到
XCode
的环境变量中,方式就是在刚才的脚本里,添加一条
export
语句:

image

完成之后,会发现编译
tgfx-vendor
也会出现同样的脚本问题,也是按照上述方式解决:

image

最后,再次编译,就会发现
PAG
源码被你成功编译了!!

1 SPI协议详解

SPI是串行外设接口(Serial Peripheral Interface)的缩写,是美国摩托罗拉公司(Motorola)最先推出的一种同步串行传输规范,也是一种单片机外设芯片串行扩展接口,是一种高速、全双工、同步通信总线,所以可以在同一时间发送和接收数据,SPI没有定义速度限制,通常能达到甚至超过10M/bps。SPI有主、从两种模式,通常由一个主模块和一个或多个从模块组成(SPI不支持多主机),主模块选择一个从模块进行同步通信,从而完成数据的交换。提供时钟的为主设备(Master),接收时钟的设备为从设备(Slave),SPI接口的读写操作,都是由主设备发起,当存在多个从设备时,通过各自的片选信号进行管理。

SPI通信原理很简单,需要至少4根线,单向传输时3根线,它们是MISO(主设备数据输入)、MOSI(主设备数据输出)、SCLK(时钟)和CS/SS(片选):

MISO
(Master Input Slave Output):主设备数据输入,从设备数据输出;

MOSI
(Master Output Slave Input):主设备数据输出,从设备数据输入;

SCLK
(Serial Clock):时钟信号,由主设备产生;

CS/SS
(Chip Select/Slave Select):从设备使能信号,由主设备控制,一主多从时,CS/SS是从芯片是否被主芯片选中的控制信号,只有片选信号为预先规定的使能信号时(高电位或低电位),主芯片对此从芯片的操作才有效。

图1 一主多从

1.1 通信原理

SPI主设备和从设备都有一个串行移位寄存器,主设备通过向它的SPI串行寄存器写入一个字节来发起一次传输。

图2 数据移位交换

SPI数据通信的流程可以分为以下几步:

1、主设备发起信号,将CS/SS拉低,启动通信。

2、主设备通过发送时钟信号,来告诉从设备进行写数据或者读数据操作(采集时机可能是时钟信号的上升沿(从低到高)或下降沿(从高到低),因为SPI有四种模式,后面会讲到),它将立即读取数据线上的信号,这样就得到了一位数据(1bit)。

3、主机(Master)将要发送的数据写到发送数据缓存区(Memory),缓存区经过移位寄存器(缓存长度不一定,看单片机配置),串行移位寄存器通过MOSI信号线将字节一位一位的移出去传送给从机,同时MISO接口接收到的数据经过移位寄存器一位一位的移到接收缓存区。

4、从机(Slave)也将自己的串行移位寄存器(缓存长度不一定,看单片机配置)中的内容通过MISO信号线返回给主机。同时通过MOSI信号线接收主机发送的数据,这样,两个移位寄存器中的内容就被交换。

其实
SPI只有主模式和从模式之分,没有读和写的说法
,外设的写操作和读操作是同步完成的。若只进行写操作,主机只需忽略接收到的字节(虚拟数据/dummy data);反之,若主机要读取从机的一个字节,就必须发送一个空字节来引发从机的传输。也就是说,你发一个数据必然会收到一个数据;你要收一个数据必须也要先发一个数据。

1.2 通信特性

1.2.1 设备选择

SPI是单主设备(Single Master)通信协议,只有一支主设备能发起通信,当SPI主设备想读/写从设备时,它首先拉低从设备对应的SS线(SS是低电平有效)。接着开始发送工作脉冲到时钟线上,在相应的脉冲时间上,主设备把信号发到MOSI实现“写”,同时可对MISO采样而实现“读”。如下图所示:

图3 逻辑分析仪数据抓取示例

低电平选择只是标准模式,也可以选择高电平有效,即IDLE时CLK为低电平,Master在要选择Slave时将CLK信号拉高。需要说明的是无论哪种方式,Master和Slave需要对选择模式配置一致。

1.2.2 设备时钟

SPI时钟特点主要包括:时钟速率、时钟极性和时钟相位三方面。

时钟速率

SPI总线上的主设备必须在通信开始时候配置并生成相应的时钟信号。从理论上讲,只要实际可行,时钟速率就可以是你想要的任何速率,当然这个速率受限于每个系统能提供多大的系统时钟频率,以及最大的SPI传输速率。

时钟极性

根据硬件制造商的命名规则不同,时钟极性通常写为CKP或CPOL。时钟极性和相位共同决定读取数据的方式,比如信号上升沿读取数据还是信号下降沿读取数据。

CKP可以配置为1或0,这意味着可根据需要将时钟的默认状态(IDLE)设置为高或低。极性反转可以通过简单的逻辑逆变器实现。须参考设备的数据手册才能正确设置CKP和CKE。

CKP = 0:时钟空闲IDLE为低电平0;

CKP = 1:时钟空闲IDLE为高电平1。

时钟相位

根据硬件制造商的不同,时钟相位通常写为CKE或CPHA。顾名思义,时钟相位/边沿,也就是采集数据时是在时钟信号的具体相位或者边沿;

CKE = 0:在时钟信号SCK的第一个跳变沿采样;

CKE = 1:在时钟信号SCK的第二个跳变沿采样。

1.2.3 四种模式

根据SPI的时钟极性和时钟相位特性可以设置4种不同的SPI通信操作模式,它们的区别是定义了在时钟脉冲的哪条边沿转换(toggles)输出信号,哪条边沿采样输入信号,还有时钟脉冲的稳定电平值(就是时钟信号无效时是高还是低),详情如下所示:

Mode0

CKP=0

CKE=0
:当空闲态时,SCK处于低电平,数据采样是在第1个边沿,也就是SCK由低电平到高电平的跳变,所以数据采样是在上升沿(准备数据),(发送数据)数据发送是在下降沿。

Mode1

CKP=0

CKE=1
:当空闲态时,SCK处于低电平,数据发送是在第2个边沿,也就是SCK由低电平到高电平的跳变,所以数据采样是在下降沿,数据发送是在上升沿。

Mode2

CKP=1

CKE=0
:当空闲态时,SCK处于高电平,数据采集是在第1个边沿,也就是SCK由高电平到低电平的跳变,所以数据采集是在下降沿,数据发送是在上升沿。

Mode3:CKP=1,CKE=1
:当空闲态时,SCK处于高电平,数据发送是在第2个边沿,也就是SCK由高电平到低电平的跳变,所以数据采集是在上升沿,数据发送是在下降沿。

图4 四种模式

图中黑线为采样数据的时刻,蓝线为SCK时钟信号。

举个例子,下图是SPI Mode0读/写时序,可以看出SCK空闲状态为低电平,主机输出数据在第一个跳变沿被从机采样,主机输入数据同理。

图5 Mode0数据采样实例

图5是SPI Mode3读/写时序,SCK空闲状态为高电平,主机输出数据在第二个跳变沿被从机采样(对应图中绿色箭头),主机输入数据同理。

1.2.4 优缺点

优点

无起始位和停止位,因此数据位可以连续传输而不会被中断;

没有像I2C这样复杂的从设备寻址系统;

数据传输速率比I2C更高(几乎快两倍);

分离的MISO和MOSI信号线,因此可以同时发送和接收数据;

极其灵活的数据传输,不限于8位,它可以是任意大小的字;

非常简单的硬件结构。从站不需要唯一地址(与I2C不同)。从机使用主机时钟,不需要精密时钟振荡器/晶振(与UART不同)。不需要收发器(与CAN不同)。

缺点

使用四根信号线(I2C和UART使用两根信号线);

无法确认是否已成功接收数据(I2C拥有此功能);

没有任何形式的错误检查,如UART中的奇偶校验位;

只允许一个主设备;

没有硬件从机应答信号(主机可能在不知情的情况下无处发送);

没有定义硬件级别的错误检查协议;

与RS-232和CAN总线相比,只能支持非常短的距离。

2 STM32相关内容

本博客基于STM32F103ZET6控制板进行所有操作,其他STM32F1型号控制板可以参考。

2.1 SPI外设简介及架构剖析

STM32的SPI外设可用作通讯的主机及从机, 支持最高的SCK时钟频率为f
pclk
/2 (STM32F103型号的芯片默认f
pclk1
为36MHz, f
pclk2
为72MHz),完全支持SPI协议的4种模式,数据帧长度可设置为8位或16位, 可设置数据MSB先行或LSB先行。它还支持双线全双工、双线单向以及单线模式。 其中双线单向模式可以同时使用MOSI及MISO数据线向一个方向传输数据,可以加快一倍的传输速度。而单线模式则可以减少硬件接线, 当然这样速率会受到影响。我们只讲解双线全双工模式。

图6 SPI架构

2.1.1 通信引脚

SPI的所有硬件架构都从图6 SPI架构图中左侧MOSI、MISO、SCK及NSS线展开的。STM32芯片有多个SPI外设, 它们的SPI通讯信号引出到不同的GPIO引脚上,使用时必须配置到这些指定的引脚,见表 STM32F10x的SPI引脚。 关于GPIO引脚的复用功能,可查阅《STM32F10x规格书》,以它为准。

图7 SPI引脚

其中SPI1是APB2上的设备,最高通信速率达36Mbtis/s,SPI2、SPI3是APB1上的设备,最高通信速率为18Mbits/s。除了通讯速率, 在其它功能上没有差异。其中SPI3用到了下载接口的引脚,这几个引脚默认功能是下载,第二功能才是IO口,如果想使用SPI3接口, 则程序上必须先禁用掉这几个IO口的下载功能。一般在资源不是十分紧张的情况下,这几个IO口是专门用于下载和调试程序,不会复用为SPI3。

2.1.2 时钟控制逻辑

SCK线的时钟信号,由波特率发生器根据“控制寄存器CR1”中的BR[0:2]位控制,该位是对f
pclk
时钟的分频因子, 对f
pclk
的分频结果就是SCK引脚的输出时钟频率,计算方法见表 BR位对f
pclk
的分频。

图8 分频配置

其中的f
pclk
频率是指SPI所在的APB总线频率, APB1为f
pclk1
,APB2为f
pckl2

通过配置“控制寄存器CR”的“CPOL位”及“CPHA”位可以把SPI设置成前面分析的4种SPI模式。

2.1.3 数据控制逻辑

SPI的MOSI及MISO都连接到数据移位寄存器上,数据移位寄存器的数据来源及目标接收、发送缓冲区以及MISO、MOSI线。 当向外发送数据的时候,数据移位寄存器以“发送缓冲区”为数据源,把数据一位一位地通过数据线发送出去;当从外部接收数据的时候, 数据移位寄存器把数据线采样到的数据一位一位地存储到“接收缓冲区”中。通过写SPI的“数据寄存器DR”把数据填充到发送缓冲区中, 通讯读“数据寄存器DR”,可以获取接收缓冲区中的内容。其中数据帧长度可以通过“控制寄存器CR1”的“DFF位”配置成8位及16位模式; 配置“LSBFIRST位”可选择MSB先行还是LSB先行。

2.1.4 整体控制逻辑

整体控制逻辑负责协调整个SPI外设,控制逻辑的工作模式根据我们配置的“控制寄存器(CR1/CR2)”的参数而改变, 基本的控制参数包括前面提到的SPI模式、波特率、LSB先行、主从模式、单双向模式等等。在外设工作时, 控制逻辑会根据外设的工作状态修改“状态寄存器(SR)”,我们只要读取状态寄存器相关的寄存器位, 就可以了解SPI的工作状态了。除此之外,控制逻辑还根据要求,负责控制产生SPI中断信号、DMA请求及控制NSS信号线。
实际应用中,我们一般不使用STM32 SPI外设的标准NSS信号线,而是更简单地使用普通的GPIO,软件控制它的电平输出,从而产生通讯起始和停止信号。

2.2 通信过程

STM32使用SPI外设通讯时,在通讯的不同阶段它会对“状态寄存器SR”的不同数据位写入参数,我们通过读取这些寄存器标志来了解通讯状态。图9 主发送器通讯过程 中的是“主模式”流程,即STM32作为SPI通讯的主机端时的数据收发过程。

图9 主模式收发流程

主模式收发流程及事件说明如下:

(1) 控制NSS信号线, 产生起始信号(图中没有画出);
(2) 把要发送的数据写入到“数据寄存器DR”中, 该数据会被存储到发送缓冲区;
(3) 通讯开始,SCK时钟开始运行。MOSI把发送缓冲区中的数据一位一位地传输出去; MISO则把数据一位一位地存储进接收缓冲区中;
(4) 当发送完一帧数据的时候,“状态寄存器SR”中的“TXE标志位”会被置1,表示传输完一帧,发送缓冲区已空;类似地, 当接收完一帧数据的时候,“RXNE标志位”会被置1,表示传输完一帧,接收缓冲区非空;
(5) 等待到“TXE标志位”为1时,若还要继续发送数据,则再次往“数据寄存器DR”写入数据即可;等待到“RXNE标志位”为1时, 通过读取“数据寄存器DR”可以获取接收缓冲区中的内容。
假如我们使能了TXE或RXNE中断,TXE或RXNE置1时会产生SPI中断信号,进入同一个中断服务函数,到SPI中断服务程序后, 可通过检查寄存器位来了解是哪一个事件,再分别进行处理。也可以使用DMA方式来收发“数据寄存器DR”中的数据。

2.3 SPI初始化结构体详解

跟其它外设一样,STM32标准库提供了SPI初始化结构体及初始化函数来配置SPI外设。 初始化结构体及函数定义在库文件“stm32f10x_spi.h”及“stm32f10x_spi.c”中,编程时我们可以结合这两个文件内的注释使用或参考库帮助文档。 了解初始化结构体后我们就能对SPI外设运用自如了,见 代码清单:SPI-1。

1 typedef struct
2 {3     uint16_t SPI_Direction;           /*设置SPI的单双向模式*/
4     uint16_t SPI_Mode;                /*设置SPI的主/从机端模式*/
5     uint16_t SPI_DataSize;            /*设置SPI的数据帧长度,可选8/16位*/
6     uint16_t SPI_CPOL;                /*设置时钟极性CPOL,可选高/低电平*/
7     uint16_t SPI_CPHA;                /*设置时钟相位,可选奇/偶数边沿采样*/
8     uint16_t SPI_NSS;                 /*设置NSS引脚由SPI硬件控制还是软件控制*/
9     uint16_t SPI_BaudRatePrescaler;   /*设置时钟分频因子,fpclk/分频数=fSCK*/
10     uint16_t SPI_FirstBit;            /*设置MSB/LSB先行*/
11     uint16_t SPI_CRCPolynomial;       /*设置CRC校验的表达式*/
12 } SPI_InitTypeDef;

这些结构体成员说明如下,其中括号内的文字是对应参数在STM32标准库中定义的宏:

  1. SPI_Direction

本成员设置SPI的通讯方向,可设置为双线全双工(SPI_Direction_2Lines_FullDuplex),双线只接收(SPI_Direction_2Lines_RxOnly), 单线只接收(SPI_Direction_1Line_Rx)、单线只发送模式(SPI_Direction_1Line_Tx)。

  1. SPI_Mode

本成员设置SPI工作在主机模式(SPI_Mode_Master)或从机模式(SPI_Mode_Slave ), 这两个模式的最大区别为SPI的SCK信号线的时序, SCK的时序是由通讯中的主机产生的。若被配置为从机模式,STM32的SPI外设将接受外来的SCK信号。

  1. SPI_DataSize

本成员可以选择SPI通讯的数据帧大小是为8位(SPI_DataSize_8b)还是16位(SPI_DataSize_16b)。

  1. SPI_CPOL和SPI_CPHA

这两个成员配置SPI的时钟极性CPOL和时钟相位CPHA,这两个配置影响到SPI的通讯模式, 关于CPOL和CPHA的说明参考前面“通讯模式”小节。

时钟极性CPOL成员,可设置为高电平(SPI_CPOL_High)或低电平(SPI_CPOL_Low )。

时钟相位CPHA 则可以设置为SPI_CPHA_1Edge(在SCK的奇数边沿采集数据) 或SPI_CPHA_2Edge(在SCK的偶数边沿采集数据) 。

  1. SPI_NSS

本成员配置NSS引脚的使用模式,可以选择为硬件模式(SPI_NSS_Hard )与软件模式(SPI_NSS_Soft ), 在硬件模式中的SPI片选信号由SPI硬件自动产生,而软件模式则需要我们亲自把相应的GPIO端口拉高或置低产生非片选和片选信号。实际中软件模式应用比较多。

  1. SPI_BaudRatePrescaler

本成员设置波特率分频因子,分频后的时钟即为SPI的SCK信号线的时钟频率。这个成员参数可设置为fpclk的2、4、6、8、16、32、64、128、256分频。

  1. SPI_FirstBit

所有串行的通讯协议都会有MSB先行(高位数据在前)还是LSB先行(低位数据在前)的问题,而STM32的SPI模块可以通过这个结构体成员,对这个特性编程控制。

  1. SPI_CRCPolynomial

这是SPI的CRC校验中的多项式,若我们使用CRC校验时,就使用这个成员的参数(多项式),来计算CRC的值。

配置完这些结构体成员后,我们要调用SPI_Init函数把这些参数写入到寄存器中,实现SPI的初始化,然后调用SPI_Cmd来使能SPI外设。

以上内容引用:
https://doc.embedfire.com/mcu/stm32/f103badao/std/zh/latest/book/SPI.html

2.4 NSS片选详解

2.4.1 输出模式

对于每个SPI的NSS可以输入,也可以输出。所谓输入,就是NSS的电平信号给自己,所谓输出,就是将NSS的电平信号发送出去,给从机。NSS配置为输出时只能用作主机,我们可以通过配置SPI_CR2寄存器的SSOE位为1。当SSOE为1时,使能SPI时,NSS就输出低电平,也就是拉低,因此当其他SPI设备的NSS引脚与它相连,必然接收到低电平,则片选成功,都成为从设备了。对应寄存器定义如下图

图10 SSOE

但是,综合实践及网上的说法,这种模式下有bug,即:

主机NSS无上拉电阻情况
使能SPI外设后,主机的NSS持续拉低,不会变高,就算关闭SPI外设也没作用。
主机NSS加上拉电阻情况
使能SPI外设后,主机的NSS拉低,关闭SPI外设后,NSS拉高。

2.4.2 输入模式

NSS输入又分为硬件输入和软件控制输入两种模式。

软件模式

1 对于SPI主机
需要设置SPI_CR1寄存器的SSM为1和SSI位为1,SSM为1是为了使能软件从设备管理。NSS有内部和外部引脚,这时候,外部引脚留作他用(可以用来作为GPIO驱动从设备的片选信号)。内部NSS引脚电平则通过SPI_CR1寄存器的SSI位来驱动。SSI位为1可使NSS内电平为高电平。STM32手册上说,要保持MSTR和SPE位为1,也就是说要保持主机模式,只有NSS接到高电平信号时,这两位才能保持置1。也就是说对于STM32的SPI,要保持为主机状态,内部输入的NSS电平必须为高。当然这里在硬件模式下也是如此。

图11 相关引脚关系图

#define SPI_Mode_Master                 ((uint16_t)0x0104)
主机模式下,会将MSTR和SSI置1,软件模式下SSM也为1,外部引脚完全被释放,可用作他用。

2 对于SPI从机

如果从机选择STM32的一个SPI,譬如主机选为SPI1,从机选为SPI2,则要按照操作手册,NSS引脚在完成字节传输之前必须连接到一个低电平信号。在软件模式下,则需要设置SPI_CR1寄存器的SSM为1(软件从设备管理使能)和SSI位为0,也就是SPI2的片选为低,则片选成功。
若从机为一个其他的SPI芯片,那么,我们可以有两种方法:一种方法,是把芯片的CS接到GND上,另一种方法是,用一个GPIO口去输出低电平来控制CS片选成功。这个GPIO可以是任何一个GPIO口,当然我们上面提到当SPI的主机配置为软件模式,外部NSS引脚留作他用了,它就是一个GPIO了,我们也可以用它。这时候,我们可以设置它推挽输出为低电平,然后用线跟从机的CS相连,那么就可以片选从芯片了。

硬件模式

对于主机,我们的NSS可以直接接到高电平,对于从机,NSS接低就可以。当然我们上面提过当一个主机的SSOE为1时,主机工作在输出模式,而且NSS拉低了,我们要让从机片选,只要将CS接到主机的NSS上,CS自动拉低。

3 实例介绍

3.1 从机

这里使用淘宝买的USB转SPI工具进行测试,为了避免广告嫌疑如需要该工具请自己在淘宝上进行搜索。

从机代码

3.2 主从互发

这里选则SPI1作为主机,SPI2作为从机,

此外,在我这边进行测试时,把从机部分NSS Pin脚的配置设置为推挽输出更为稳定,暂时不清楚原理。

主从代码

3.3 flash读写

该实例的例子网上比较多,就不再单独进行上传了!

题目链接:-
P1098 [NOIP2007 提高组] 字符串的展开

题目叙述:

[NOIP2007 提高组] 字符串的展开

题目描述

在初赛普及组的“阅读程序写结果”的问题中,我们曾给出一个字符串展开的例子:如果在输入的字符串中,含有类似于
d-h
或者
4-8
的字串,我们就把它当作一种简写,输出时,用连续递增的字母或数字串替代其中的减号,即,将上面两个子串分别输出为
defgh

45678
。在本题中,我们通过增加一些参数的设置,使字符串的展开更为灵活。具体约定如下:

(1) 遇到下面的情况需要做字符串的展开:在输入的字符串中,出现了减号
-
,减号两侧同为小写字母或同为数字,且按照
ASCII
码的顺序,减号右边的字符严格大于左边的字符。

(2) 参数 p1:展开方式。p1=1 时,对于字母子串,填充小写字母;p1=2 时,对于字母子串,填充大写字母。这两种情况下数字子串的填充方式相同。p1=3 时,不论是字母子串还是数字字串,都用与要填充的字母个数相同的星号
*
来填充。

(3) 参数 p2:填充字符的重复个数。p2=k 表示同一个字符要连续填充 k个。例如,当p2=3时,子串
d-h
应扩展为
deeefffgggh
。减号两边的字符不变。

(4) 参数 p_3:是否改为逆序:p3=1 表示维持原来顺序p3=2 表示采用逆序输出,注意这时候仍然不包括减号两端的字符。例如当 p1=1、p2=2、p3=2 时,子串
d-h
应扩展为
dggffeeh

(5) 如果减号右边的字符恰好是左边字符的后继,只删除中间的减号,例如:
d-e
应输出为
de

3-4
应输出为
34
。如果减号右边的字符按照
ASCII
码的顺序小于或等于左边字符,输出时,要保留中间的减号,例如:
d-d
应输出为
d-d

3-1
应输出为
3-1

输入格式

共两行。

第 1 行为用空格隔开的 3 个正整数,依次表示参数 p1,p2,p3。

第 2 行为一行字符串,仅由数字、小写字母和减号
-
组成。行首和行末均无空格。

输出格式

共一行,为展开后的字符串。

样例 #1

样例输入 #1

1 2 1
abcs-w1234-9s-4zz

样例输出 #1

abcsttuuvvw1234556677889s-4zz

样例 #2

样例输入 #2

2 3 2
a-d-d

样例输出 #2

aCCCBBBd-d

提示

40% 的数据满足:字符串长度不超过 5。

100% 的数据满足:1<=p1<=3;1<=p2<=8,1<=p3<=2。字符串长度不超过 100。

NOIP 2007 提高第二题

思路

这题看起来似乎比较复杂,我们来细心捋一下这道题的逻辑是怎么样的。

首先,我们要搜索到
-
,并且
-
的左右两边都要有元素,这样我们才进行填充,因此,我们在接收一个字符串以后,我们只需要遍历它的第二个元素到倒数第二个元素。

其次,我们观察到,p2这个数字,只决定了我们填充的每一个字符的个数,因此p2应该与大体分类上无关,只与我们构造我们要填充的字符串时有关

再一个就是,p3的值,是决定填充后反不反向的问题,因此,我们可以直接构造正向填充的字符串,如果p3==2,我们就反转一下构造的字符串,并插入到原本
-
所对应的那个位置就行了

库函数

思路讲解完了,我们开始讲库函数,这题使用库函数能够大大降低我们写代码的时间,建议大家好好了解库函数

头文件
#include<cctype>
中的库函数

1、toupper(x) 如果x是小写字母,将其转换成大写字母

2、tolower(x) 如果x是大写字母,将其转换成小写字母

3、isalpha(x) 判断x是否为字母

4、isdigit(x) 判断x是否为数字

5、islower(x) 判断x是否为小写字母

6、isupper(x) 判断x是否为大写字母

7、isalnum(x) 判断x是否为字母或数字

8、ispunct(x) 判断x是否为标点符号

9、isspace(x) 判断x是否为空格

头文件
#include<string>
中的库函数

1、s.erase(x,y) 表示将字符串s从x位置起删除y个字符

2、s.insert(x,y) 表示将字符串y(或字符y)插入到s的x位置处

3、s.push_back(x) 表示在s的末尾插入字符x

4、reverse(s.begin(),s.end()) 将字符串s翻转

建议读者好好深入了解一下string类中提供的成员函数

步骤拆解

首先,我们要输入一个字符串和三个数字,然后遍历这个字符串,从第二个元素直到倒数第二个元素,如果碰见
-
,并且
-
的左右两边是小写字母(或数字),并且左边小写字母(或数字)
ASCII码小于右边的,我们才进行处理。

然后,我们明确一个点,我们需要删除
-
位置的这个
-
,然后插入一个新的字符串
current
,接下来就是我们构造这个新的字符串的过程.我们构造
current
分为三种情况

删除i位置的
-
的代码为:

			//我们无论填充什么字符,或是不填充字符,都需要删掉i这个位置的'-’,所以放在最最前面
			s.erase(i,1);

1.
p1==1
,我们会填充小写字母,然后根据p3的值判断是否需要反转这个字符串
current
,接下来就是构造
current
的过程

我们从s[i-1]的下一个字母开始,直到s[i+1]的前一个字母,每个字母循环p2次,并且都是小写字母,代码如下:

string current;
//构造中间需要填充的那个字符串
for (char c = s[i - 1] + 1; c <= s[i] - 1; c++) {
				char a=c;
				for(int i=1;i<=p2;i++) current+=a;
}

然后,根据p3的值,判断我们是否需要进行反转,代码如下:

					//如果p3==2,则需要反转,否则不需要
					if (p3 == 2)  reverse(current.begin(), current.end());

最后,我们在i的这个位置插入字符串即可

					//在i的这个位置插入字符串
					s.insert(i, current);

做完了
p1==1
的步骤,
p1==2

p1==3
就迎刃而解了,只需要改变字符a的值,就可以了

2.
p1==2
的情况

				else if (p1 == 2) {
					string current;
					for (char c = s[i - 1] + 1; c <= s[i] - 1; c++) {
						char a = toupper(c);
						for (int i = 1; i <= p2; i++) current += a;
					}
					if (p3 == 2)	reverse(current.begin(), current.end());
					s.insert(i, current);
				}

3.
p1==3
的情况

				else if (p1 == 3) {
					string current;
					for (char c = s[i - 1] + 1; c <= s[i] - 1; c++) {
						char a = '*';
						for (int i = 1; i <= p2; i++) current += a;
					}
					if (p3 == 2) reverse(current.begin(), current.end());
					s.insert(i, current);
				}

最后,我们直接输出这个修改后的字符串就可以了

最终代码

#include<iostream>
#include<cctype>
#include<algorithm>
using namespace std;
int main()
{
	int p1, p2, p3;
	cin >> p1 >> p2 >> p3;
	string s; cin >> s;
	//从第二个元素开始,遍历到倒数第二个元素结束,因为开头和结尾的 '-'不需要管
	for (int i = 1; i < s.size() - 1; i++) {
		//只有当-号左右是字符(或数字),并且左边小于右边时,我们才处理
		if (s[i] == '-' && ((islower(s[i - 1]) && islower(s[i + 1]) && s[i - 1] < s[i + 1]) || (isdigit(s[i - 1]) && isdigit(s[i + 1]) && s[i - 1] < s[i + 1]))) {
			//我们无论填充什么字符,或是不填充字符,都需要删掉i这个位置的'-’,所以放在最最前面
			s.erase(i,1);
			//填充小写字母
				if (p1 == 1) {
					string current;
					//构造中间需要填充的那个字符串
					for (char c = s[i - 1] + 1; c <= s[i] - 1; c++) {
						char a=c;
						for(int i=1;i<=p2;i++) current+=a;
					}
					//如果p3==2,则需要反转,否则不需要
					if (p3 == 2)  reverse(current.begin(), current.end());
					//在i的这个位置插入字符串
					s.insert(i, current);
				}
				else if (p1 == 2) {
					string current;
					for (char c = s[i - 1] + 1; c <= s[i] - 1; c++) {
						char a = toupper(c);
						for (int i = 1; i <= p2; i++) current += a;
					}
					if (p3 == 2)	reverse(current.begin(), current.end());
					s.insert(i, current);
				}
				else if (p1 == 3) {
					string current;
					for (char c = s[i - 1] + 1; c <= s[i] - 1; c++) {
						char a = '*';
						for (int i = 1; i <= p2; i++) current += a;
					}
					if (p3 == 2) reverse(current.begin(), current.end());
					s.insert(i, current);
				}
		}
	}
	cout << s << endl;
	return 0;

}



《Scratch作品-巴黎2024奥运会》是一款以巴黎2024年奥运会为主题的互动作品,专为儿童和青少年设计。通过Scratch编程语言,这个作品生动地再现了奥运会的精彩瞬间,结合了动画、声音和互动元素,让用户仿佛置身于巴黎的奥运赛场。玩家可以参与各种虚拟的奥运项目,学习奥运精神,了解各国文化,并激发他们对编程和创意表达的兴趣。这个作品不仅是一种娱乐方式,更是一种教育工具,鼓励年轻人通过编程和创作表达他们对体育和奥运会的热爱。

下载地址:小虎鲸scratch资源站

想要深入了解和体验《Scratch作品-巴黎2024奥运会》,您可以访问小虎鲸Scratch资源站下载该作品的源码。小虎鲸Scratch资源站是一个专门提供优质Scratch项目和资源的平台,致力于为广大编程爱好者和教育者提供丰富的学习资料和创作灵感。在这里,您不仅可以找到《Scratch作品-巴黎2024奥运会》的源码,还能探索更多精彩的Scratch作品,与全球的编程爱好者交流和分享创作心得。

Command,即命令,具体而言,指的是实现了 ICommand 接口的对象。此接口要求实现者包含这些成员:

1、CanExecute 方法:确定该命令是否可以执行,若可,返回 true;若不可,返回 false;

2、CanExecuteChanged 事件:发送命令(命令源)的控件可以订阅此事件,当命令的可执行性改变时能得到通知;

3、Execute 方法:执行命令时调用此方法。可以将命令逻辑写在此方法中。

命令源(ICommandSource)

发送命令的控件就是命令源,例如常见的菜单项、按钮等。即命令是怎么触发的,这肯定与用户交互有关的。无交互功能的控件一般不需要发送命令。有命令源就会有命令目标,若命令源是发送者,那么命令目标就是命令的接收者(命令最终作用在谁身上)。比如,单击 K 按钮后清空 T 控件中的文本。则,K是命令源,T就是命令目标。这样举例相信大伙伴们能够理解,老周就不说太多,理论部分越简单越好懂。这里没什么玄的,只要你分清角色就行,谁发出,谁接收。

命令必须有触发者,所以,源是必须的,并且,作为命令源的控件要实现 ICommandSource 接口,并实现三个成员:

1、Command: 要发送的命令对象;

2、CommandParameter:命令参数。这个是任意对象,由你自己决定它是啥,比如,你的命令是删除某位员工的数据记录,那么,这个参数可能是员工ID。这个参数是可选的,当你的命令逻辑需要额外数据时才用到,不用默认为 null 就行了;

3、CommandTarget:目标。命令要作用在哪个控件上。其实这个也是可选的,命令可以无目标控件。比如,删除个员工记录,如果知道要删除哪记录,那这里不需要目标控件。当然,如果你的逻辑是要清空文本框的文本,那目标控件是 TextBox。这个取决你的代码逻辑。

像 Button、MenuItem 这些控件,就是命令源,都实现 ICommandSource 接口。

命令逻辑

命令逻辑就是你的命令要干的活。咱们做个演示。

下面示例将通过命令来删除一条学生记录。Student 类的定义如下:

    public classStudent
{
public string? Name { get; set; } = string.Empty;public int ID { get; set; }public int Age { get; set; }public string Major { get; set; } = string.Empty;
}
public classStudentViewManager
{
private static readonly ObservableCollection<Student> _students = new ObservableCollection<Student>();staticStudentViewManager()
{
_students.Add(
newStudent()
{
ID
= 1,
Name
= "小陈",
Age
= 20,
Major
= "打老虎专业"});
_students.Add(
newStudent()
{
ID
= 2,
Name
= "小张",
Age
= 21,
Major
= "铺地砖专业"});
_students.Add(
newStudent()
{
ID
= 3,
Name
= "吕布",
Age
= 23,
Major
= "坑义父专业户"});
}
public static ObservableCollection<Student>Students
{
get { return_students; }
}
}

然后,定义一个实现 ICommand 接口的类。

    public classDelStuCommand : ICommand
{
public event EventHandler?CanExecuteChanged;public bool CanExecute(object?parameter)
{
return !(StudentViewManager.Students.Count == 0);
}
public void Execute(object?parameter)
{
Student
? s = parameter asStudent;if (s == null)return;

StudentViewManager.Students.Remove(s);
}
}

执行此命令需要参数,好让它知道要删除哪条学生记录。

下面 XAML 中,ListBox 控件显示学生列表,按钮引用上述命令对象。

    <Grid>
        <Grid.RowDefinitions>
            <RowDefinition/>
            <RowDefinitionHeight="auto"/>
        </Grid.RowDefinitions>
        <Grid.Resources>
            <local:DelStuCommandx:Key="cmd"/>
        </Grid.Resources>
        <ButtonContent="删除"Grid.Row="1"Command="{StaticResource cmd}"CommandParameter="{Binding ElementName=tc, Path=SelectedItem}"/>
        <ListBoxx:Name="tc"Grid.Row="0">
            <ItemsControl.ItemTemplate>
                <DataTemplateDataType="local:Student">
                    <TextBlock>
                        <RunText="{Binding Name}"/>
                        <Span> | </Span>
                        <RunText="{Binding Major}"Foreground="Blue"/>
                    </TextBlock>
                </DataTemplate>
            </ItemsControl.ItemTemplate>
        </ListBox>               
    </Grid>

Button 类实现了 ICommandSource 接口,通过 CommandParameter 属性指定要传递给命令的参数。

运行程序后,在 ListBox 中选择一项,然后点“删除”按钮。

删除后,只剩下两项。重复以下操作,当所有记录都删除后,“删除”按钮就会被禁用。

从这个示例可以了解到,命令可以把某种行为封装为一个单独的整体。这样能增加其可复用性,按钮、菜单、工具栏按钮都可以使用同一个命令,实现相同的功能。

路由命令与 CommandBinding

实现 ICommand 接口虽然简单易用,但它也有一个问题:如果我的程序里有很多命令逻辑,那我就要定义很多命令类。比如像这样的,你岂不是要定义几十个命令类。

这样就引出 RoutedCommand 类的用途了。

RoutedCommand 类实现了 ICommand 接口,它封装了一些通用逻辑,具体逻辑将以事件的方式处理。RoutedCommand 类的事件均来自 CommandManager 类所注册的路由(隧道)事件。即

1、CanExecute 和 PreviewCanExecute 事件:当要确定命令是否能够执行时会发生该事件。Preview 开头的表示隧道事件。可能有大伙伴不太记得这个名词。其实,路由事件和隧道事件本质一样,只是传递的方向不同。挖隧道的时候是不是从外头往里面钻?所以,隧道事件就是从外层元素往里面传播;路由事件就相反,从里向外传播。

2、Executed 和 PreviewExecuted 事件:咱们可以处理这事件,然后将自己要实现的命令逻辑写上即可。

可见,有了 RoutedCommand,咱们就不需要定义一堆命令类了,而是全用它,代码逻辑在 Executed 事件中写。这里也包括 RoutedUICommand  命令,这个类只不过多了个 Text 属性,用来指定关联的文本罢了,文本会显示在菜单上。

不过,咱们在使用时不会直接去处理 RoutedCommand 类的事件,而是配合另一个类—— CommandBinding 来使用。有了它,事件才能冒泡(或下沉),也就是可向上或向下传播。传播的路径是从目标对象(Command Target)开始,到最后能捕捉到事件的 CommandBindings 结束。这个不理解不重要,后面咱们用例子说明。

下面咱们再做一个示例。这个例子中,咱们用四个菜单项来改变矩形的颜色。

由于现在用的是 RoutedCommand 类,我们不需要定义命令类了,所以能在 XAML 文档中直接把命令声明在资源中。

    <Window.Resources>
        <!--命令列表-->
        <RoutedCommandx:Key="greenCmd" />
        <RoutedCommandx:Key="silverCmd" />
        <RoutedCommandx:Key="redCmd" />
        <RoutedCommandx:Key="blackCmd" />
    </Window.Resources>

我们定义一组菜单,以及一个矩形。

    <Grid>
        <Grid.RowDefinitions>
            <RowDefinitionHeight="auto"/>
            <RowDefinition/>
        </Grid.RowDefinitions>
        <Menu>
            <MenuItemHeader="颜色">
                <MenuItemHeader="绿色"Command="{StaticResource greenCmd}"CommandTarget="{Binding ElementName=rect}"/>
                <MenuItemHeader="银色"Command="{StaticResource silverCmd}"CommandTarget="{Binding ElementName=rect}"/>
                <MenuItemHeader="红色"Command="{StaticResource redCmd}"CommandTarget="{Binding ElementName=rect}"/>
                <MenuItemHeader="黑色"Command="{StaticResource blackCmd}"CommandTarget="{Binding ElementName=rect}"/>
            </MenuItem>
        </Menu>
        <RectangleGrid.Row="1"Height="80"Width="100"Name="rect"Fill="Blue" />
    </Grid>

网格分两行,上面是菜单,下面是矩形。每个菜单项的 Command 属性已经引用了所需的命令对象。CommandTarget 属性通过绑定引用矩形对象。这里要注意,Target 要求的是实现 IInputElement 接口的类型。可见,不是所有对象都能充当目标的。Rectangle 类可以作为命令目标。

这时不要直接处理 RoutedCommand 类的事件,而是要借助 CommandBinding。UIElement 的子类都继承 CommandBindings 集合,所以放心用,大部分界面元素都可以用。本例中,我们在 Grid 上写 CommandBinding。

    <Grid>
        <Grid.RowDefinitions>
            <RowDefinitionHeight="auto"/>
            <RowDefinition/>
        </Grid.RowDefinitions>
        <Grid.CommandBindings>
            <CommandBindingCommand="{StaticResource greenCmd}" 
                                CanExecute="OnRectCanExecut"
                                Executed="OnGreenCmdExe"/>
            <CommandBindingCommand="{StaticResource silverCmd}"CanExecute="OnRectCanExecut"Executed="OnSilverCmdExe"/>
            <CommandBindingCommand="{StaticResource redCmd}"CanExecute="OnRectCanExecut"Executed="OnRedCmdExe"/>
            <CommandBindingCommand="{StaticResource blackCmd}"CanExecute="OnRectCanExecut"Executed="OnBlackCmdExe" />
        </Grid.CommandBindings>
        <Menu>……</MenuItem>
        </Menu>
        <RectangleGrid.Row="1"Height="80"Width="100"Name="rect"Fill="Blue" />
    </Grid>

在使用 CommandBinding 时,注意 Command 所引用的命令时你要用的,这里就是要和四个菜单项所引用的命令一致,不然,CanExecute 和 Executed 事件不起作用(命令不能正确触发)。如果事件逻辑相同,可以共用一个 handler,比如上面的,CanExecute 事件就共用一个处理方法。

接下来,我们处理一下这些事件。

private void OnGreenCmdExe(objectsender, ExecutedRoutedEventArgs e)
{
Rectangle rect
=(Rectangle)e.OriginalSource;
rect.Fill
= newSolidColorBrush(Colors.Green);
}
private void OnSilverCmdExe(objectsender, ExecutedRoutedEventArgs e)
{
Rectangle rect
=(Rectangle)e.OriginalSource;
rect.Fill
= newSolidColorBrush(Colors.Silver);
}
private void OnRedCmdExe(objectsender, ExecutedRoutedEventArgs e)
{
Rectangle rect
=(Rectangle)e.OriginalSource;
rect.Fill
= newSolidColorBrush(Colors.Red);
}
private void OnBlackCmdExe(objectsender, ExecutedRoutedEventArgs e)
{
Rectangle rect
=(Rectangle)e.OriginalSource;
rect.Fill
= newSolidColorBrush(Colors.Black);
}
private void OnRectCanExecut(objectsender, CanExecuteRoutedEventArgs e)
{
e.CanExecute
= (e.OriginalSource != null && e.OriginalSource isRectangle);
}

在 OnRectCanExecut 方法,本例的判断方式是只要命令目标不为空,并且是矩形对象,就允许执行命令。e.CanExecute 属性就是用来设置一个布尔值,以表示能不能执行命令。

代码很简单,老周不多解释了。重点说的是,引发这些事件的源头是 Command Target。即 OriginalSource 引用的就是 Rectangle。事件路径是从目标对象开始向上冒泡的——说人话就是从 Rectangle 开始向上找 CommandBinding,不管是哪个层次上的 CommandBinding,只要事件和命令是匹配的,就会触发。

我们不妨这样改,把 Grid 下的后两个 CommandBinding 向上移,移到 Window 对象下。

    <Window.CommandBindings>
        <CommandBindingCommand="{StaticResource redCmd}"
                                CanExecute="OnRectCanExecut"
                                Executed="OnRedCmdExe"/>       <CommandBindingCommand="{StaticResource blackCmd}"
                               CanExecute="OnRectCanExecut"
                                Executed="OnBlackCmdExe" />
    </Window.CommandBindings>
    <Grid>
        <Grid.RowDefinitions>
            <RowDefinitionHeight="auto"/>
            <RowDefinition/>
        </Grid.RowDefinitions>
        <Grid.CommandBindings>
            <CommandBindingCommand="{StaticResource greenCmd}" 
                                CanExecute="OnRectCanExecut"
                                Executed="OnGreenCmdExe"/>           <CommandBindingCommand="{StaticResource silverCmd}"
                                CanExecute="OnRectCanExecut"
                                Executed="OnSilverCmdExe"/>
        </Grid.CommandBindings>
        <Menu>……</Menu>……</Grid>

运行后,你会发现,四个菜单都能用。

从 Rectangle 开始向上冒泡,先是在 Grid 元素上找到两个 CommandBinding,匹配,用之;再往上,在 Window 元素上又找到两个,匹配,用之。所以,最后就是四个都能用。因此,路由是以 Rectangle 为起点向上冒泡,直到 Window 对象。

其实,上面几个 Executed 事件也可以合并到一个方法中处理,只要用 CommandParameter 区分哪种颜色就行。

 private void OnCmdExecuted(objectsender, ExecutedRoutedEventArgs e)
{
Rectangle rect
=(Rectangle)e.OriginalSource;//获取参数值 int val =Convert.ToInt32(e.Parameter); //根据参数选择颜色 SolidColorBrush brush = new();switch(val)
{
case 0:
brush.Color
=Colors.Green;break;case 1:
brush.Color
=Colors.Silver;break;case 2:
brush.Color
=Colors.Red;break;case 3:
brush.Color
=Colors.Black;break;default:
brush.Color
=Colors.Blue;break;
}
rect.Fill
=brush;
}

在 XAML 文档中,替换前面设置的事件 handler,并在菜单项中设置 CommandParameter。

<CommandBindingCommand="{StaticResource redCmd}"CanExecute="OnRectCanExecut"Executed="OnCmdExecuted"/>
<CommandBindingCommand="{StaticResource blackCmd}"CanExecute="OnRectCanExecut"Executed="OnCmdExecuted" />
<CommandBindingCommand="{StaticResource greenCmd}"CanExecute="OnRectCanExecut"Executed="OnCmdExecuted"/>
<CommandBindingCommand="{StaticResource silverCmd}"CanExecute="OnRectCanExecut"Executed="OnCmdExecuted"/>
<MenuItemHeader="绿色"Command="{StaticResource greenCmd}"CommandTarget="{Binding ElementName=rect}"CommandParameter="0"/>
<MenuItemHeader="银色"Command="{StaticResource silverCmd}"CommandTarget="{Binding ElementName=rect}"CommandParameter="1"/>
<MenuItemHeader="红色"Command="{StaticResource redCmd}"CommandTarget="{Binding ElementName=rect}"CommandParameter="2"/>
<MenuItemHeader="黑色"Command="{StaticResource blackCmd}"CommandTarget="{Binding ElementName=rect}"CommandParameter="3"/>

指定快捷按键

命令的好处不只是可以多个源共享代码逻辑,还支持快捷键绑定。这就要用到 InputBinding 对象了,仔细看,发现这个类实现了 ICommandSource 接口。

public class InputBinding : System.Windows.Freezable, System.Windows.Input.ICommandSource

因此,它也可以与命令关联,只要 InputBinding 被触发,关联的命令也会执行。下面咱们为上面的示例添加快捷键。

<Window.InputBindings>
    <KeyBindingGesture="ctrl+shift+1"Command="{StaticResource greenCmd}"CommandTarget="{Binding ElementName=rect}"CommandParameter="0"/>
    <KeyBindingGesture="ctrl+shift+2"Command="{StaticResource silverCmd}"CommandTarget="{Binding ElementName=rect}"CommandParameter="1"/>
    <KeyBindingGesture="ctrl+shift+3"Command="{StaticResource redCmd}"CommandTarget="{Binding ElementName=rect}"CommandParameter="2"/>
    <KeyBindingGesture="CTRL+SHIFT+4"Command="{StaticResource blackCmd}"CommandTarget="{Binding ElementName=rect}"CommandParameter="3"/>
</Window.InputBindings>

UIElement 类的派生类都继承了 InputBindings 集合,通常我们是把 InputBinding 放到窗口的集合中。实际上这里可以把 InputBinding 写在 Grid.InputBindings 中。前面咱们提过,事件是从 Target 对象向上冒泡的,所以在窗口上定义 InputBinding 或 CommandBinding,可以尽可能地捕捉到命令事件。

InputBinding 只是基类,它有两个派生类—— KeyBinding,MouseBinding。不用老周解释,看名识类,你都猜到它们是干吗用的了。示例中用到的是快捷键,所以用 KeyBinding。快捷键在 XAML 中有两种声明方法:

1、如本例所示,直接设置 Gesture 属性。使用按键的字符串形式,不分大小写,按键之间用“+”连接,如 Ctrl + C。这种方法把修改键和普通键一起定义,方便好用;

2、修改键和按键分开定义。即使用 Key 和 Modifiers 属性,Key 指定普通键,如“G”;Modifiers 指定修改键,如 "Ctrl + Alt"。因此,本示例的快捷键也可以这样定义:

<KeyBindingModifiers="Ctrl+Shift"
            Key="D4"Command="{StaticResource blackCmd}"CommandTarget="{Binding ElementName=rect}"CommandParameter="3"/>

这里的 Key 属性比较特别,不能直接写“4”,因为无法从字符串“4”转换为 Key 枚举,会报错,可以指定为“D4”、“D5”等。这里所指定的数字键是大键盘区域的数字(QWERTYUIOP 上面那排),不是右边小键盘的数字键。小键盘要用"NumPad4"。小数字键盘跟有些修改键组合后无效,经老周测试,Shift、Alt、Win这些键都无效,Ctrl 可以。所以,还是用字母键靠谱些,也不用区分大小键盘区域。

重点:Key + Modifiers 方式与 Gesture 方式只能二选一,不能同时使用,会产生歧义

CommandTarget 为什么是可选的

前面提到,命令目标是可选的,可以不指定,为什么呢?这就要看命令源的处理方式了。我们可以看看 WPF 内部的处理。

internal static boolCanExecuteCommandSource(ICommandSource commandSource)
{
ICommand command
=commandSource.Command;if (command != null)
{
object parameter =commandSource.CommandParameter;
IInputElement target
=commandSource.CommandTarget;

RoutedCommand routed
= command asRoutedCommand;if (routed != null)
{
if (target == null) { target= commandSource asIInputElement; } returnrouted.CanExecute(parameter, target);
}
else{returncommand.CanExecute(parameter);
}
}
return false;
}

如果命令是 RoutedCommand,且目标是存在的,就触发 CanExe 事件;如果未指定目标,则将命令源作为目标。

如果命令不是 RoutedCommand,则直接无视目标。

所以,总的来说,Target 就是可选的。不过,对于非路由的命令,默认会把
键盘焦点所在的控件
视为目标。

现在,老周相信大伙伴们都会使用命令了。在实际使用中,你还可以把命令直接封装进 Model 类型中,比如作为一个公共属性。MVVM不是很喜欢这样用的吗?这样封装确实很方便的,尤其是你有N个窗口,这些窗口可能都出现一个“编辑员工信息”的菜单或按钮。如果你的员工信息模型中直接封装了命令,在命令的逻辑中打开编辑对话框。这样就省了许多重复代码了,而且这 N 个窗口的代码也变得简洁了,你甚至都不用给按钮们处理 Click 事件。

----------------------------------------------------------------------------------------------------------------------------------------------

最后,解释一下老周最近写水文为什么效率这么低。因为老周最近很光荣,经朋友介绍,以 A 公司员工的名义,被派遣到 B 集团总部的开发部门。就类似于外包之类了吧,就是过去那里干一段时间。这关系很复杂吧。其实老周本来是不想去的,但还是给朋友 45% 的面子(唉,人最可悲的就是总觉得面子可以当饭吃),就答应了,顺便赚点生活费。包吃不包住,来回就用网约车。因为这“一段时间”太模糊,租房子不好弄,交押金什么的,时间又不确定,咋整。所以,只好打车,费用找他们公司报销。

如果你常被外派的话,可能知道这活是不好干的。你想想,人家为什么要找你上门?就是因为他们自己解决不了问题,你过去就是负责啃硬骨头的。由于签了保密协议,老周不能说是什么项目。总之项目很大,TM的复杂,主要帮他们做优化。他们的办公室跟菜市场似的,每天很热闹,上班可以走来走去,聊天扯蛋。氛围不错,你到处逛领导也不管,反正你得完成进度。老周粗略估算,一张桌子坐 8 个人,办公室很大,有6列17行,能坐 6*17*8 个人,整栋楼有 2313 人(听见他们广播中是这样说的),不知道算不算我们外包人员。想想他们的开发团队有多大了。

毕竟是大集团公司,在东南亚和欧洲有很多个生产基地。所以他们的开发团队本来设立是为子公司的工厂开发软件系统的。不过,在食堂听内部人员说,这几年他们除了自己集团内的项目,外面的杂七杂八的项目也接,项目很多,而且很乱,大家都干得很无语,经常都分不清哪个项目跟哪个项目。一个项目还建了很多分支,很多版本。刚到那里的时候,也把老周整得很无语,项目名称都是【三个字母+数字+一个字母】表示,最后一个字母表示版本分支。看任务文档,然后在源码服务器上找项目都找得头晕。

本来以为这样的大公司,代码应该写得很规范的。谁曾想,他们完全就是“能运行就好,其他免谈”,代码是真的写得一团乱,甚至都不知道经过多少手了,看里面的注释,最早有 2013 年的修改记录。而且注释里面是繁、中文,英文,日文,还有其他不知道什么鸟文的都有。起初我还以为是乱码。估计什么越南语都有。这实打实的是混血代码。

说实话,外派到别人的大团队里真的很郁闷。他们自己人一个圈子,喜欢欺负新人。当然,不是物理上的欺负,毕竟老周小时候跟江湖骗子练过两年的,真打起来的话,老周可以一打五。老周指的是他们总把些难搞的任务交给你做——也是意料之中的。所以,根据老周多年忽悠人的经验,外派到其他公司一定要学会“装糊涂”。

啥意思呢?不是叫你装傻子,而且要装菜。你不能表现得像个大神,不然他们会丢更多的硬骨头让你啃(人不如狗,现在狗都不啃骨头了)。所以你要装成菜鸟,但不能太菜。派出公司在介绍时肯定会吹牛你有多少个世纪的开发经验的,如果装得太小白,他们就会发现你是不想干活,故意装。装要装得有点菜,不能太菜。比如,某个东西老周其实用 30 分钟就能做出来的,我硬要做他个2小时。本来一天能完事的,非要做个两天。如果经理问,就说“这个 RadGridView 控件和 WinForm 是不兼容的,如果换 UI,要处理1、2、3、4……” 总之,有很大的难度,需要不可预估的时间去完成。三天能搞好的,就说一个星期。拖拖进度,可以减轻负担。因为老周很累,白天打车过去帮别人搞项目,晚上回去还要改另外两个项目。白天脑子嗡嗡响,晚上脑子嗞嗞响。

放慢速度来做,等派遣约定的时间到了,直接闪退。反正一两个月,拖拖拉拉就扛过了。没必要玩命给别人好印象的,反正跟他们没混熟就走人了,那个菜市场一样的办公室那么大,谁记得你啊。

今天的水文就写到这儿了,明天又要去菜市场混日子了,还有一个半月,很快就熬过去。