我的 WinClock 项目系列之三 (Mediator模式的应用)
关机的实现:关机也是调用Windows API 实现的,具体代码参考上次列出的清单
PInvokeService.DoExitWin(int) 函数。
值得一提的是 关机对话框调用了 Windows 中一个未公开的 API, 我们不知道这个函数
的名字,但是我们知道它的地址,我使用 ISO C++ 进行了封装,代码如下:
#include <windows.h>2

3
namespace CPPCode {4
namespace Shutdown {5
typedef int (CALLBACK *SHUTDOWNDLG) (int); 6

7
extern "C" {8
__declspec(dllexport) void ShowShutdownDialog();9
} 10
11
void ShowShutdownDialog() { 12
HINSTANCE hInst = LoadLibrary(LPCWSTR(L"shell32.dll")); 13
SHUTDOWNDLG SHShutDownDialog; 14

15
if(hInst != NULL) { 16
// Shutdown dialog API is not publicized by Microsoft but we know17
// It's ID is 6018
SHShutDownDialog = (SHUTDOWNDLG)GetProcAddress(hInst, (LPSTR)60); 19
SHShutDownDialog(0); 20
FreeLibrary(hInst);21
}22
} 23
}24
}25

编译生成的 dll 是 CPPCode.Shutdown.dll
然后你可以在 PInvokeService.cs 里面找到这样的代码:
[DllImport("CPPCode.Shutdown.dll", ExactSpelling = true, SetLastError = true)]
public static extern void ShowShutdownDialog();
抛开具体细节讨论抽象,现在可以开始正式讲本次的内容了。
Mediator模式的应用
动机(Motivation):
在软件的构建过程中,经常会出现多个对象互相关联交互的情况,对象之间常常会维持一种
复杂的引用关系,如果遇到一些需求的更改,这种直接的引用关系将面临不断的变化。
在这种情况下,我们可以使用一个“中介对象”来管理对象间的关联关系,避免相互的对象之间
紧耦合引用关系,从而更好地抵御变化。
意图(Intent):
用一个中介对象来封装一系列对象交互。中介者使各对象不需要显示的相互引用,从而使其
耦合松散,而且可以独立地改变它们之间的交互。
————《设计模式》GOF
我们抛开这些精典的理论,看看如何把我们的功能细节分离到一系列 class 中,以及让菜单工作。
先总体上看一下最后的类图:

点击下载完整类图
Element 和 Mediator 都是抽象类,但他们之间存在很强的耦合关系, Element 依赖 Mediator 对象,
同时Mediator对象内部包含 Element 的集合。因此 Mediator 也依赖 Element,Element 了类化就
可以在这些类中实现我们的功能细节。那就先看看这两个抽象类吧:
// Element.cs2
internal abstract class Element : IDisposable {3
private ICommand theCommand;4
protected Mediator mediator;5

6
public Element(Mediator mediator) {7
this.mediator = mediator;8
this.mediator.AddElement(this);9
}10

11
~Element() {12
Dispose(false);13
}14

15
protected MainForm mainForm {16
get {17
return mediator.MainForm;18
}19
}20

21
protected ICommand command {22
get {23
return theCommand;24
}25
set {26
theCommand = value;27
if (theCommand != null) {28
RegisterEvents(false);29
RegisterEvents(true);30
}31
}32
}33

34
public object Source {35
get {36
return this.command.Source;37
}38
}39

40
protected void NotifyStatusChanged() {41
if (mediator != null) {42
mediator.Notify();43
}44
}45

46
protected ClockOption clockOpt {47
get {48
return mainForm.ClockOption;49
}50
}51

52
protected RemindOperate remindOperate {53
get {54
return mainForm.RemindOperate;55
}56
}57

58
// Only dispose the item, do not dispose the mediator59
// because the mediator is sigalton, it need dispose 60
// only once61
protected virtual void Dispose(bool disposing) {62
if (disposing) {63
// Do not move this outside of the if block, because64
// memory of the element may have been collected by GC65
try {66
RegisterEvents(false);67
this.command.Dispose();68
} finally {69
this.command = null;70
}71
}72
}73

74
protected abstract void OnExecute();75
protected internal virtual void OnStatusChanged() {76
}77

78
private void RegisterEvents(bool register) {79
if (register) {80
this.command.Execute += CommandOnExecuted;81
} else {82
this.command.Execute -= CommandOnExecuted;83
}84
}85

86
private void CommandOnExecuted(object sender, EventArgs args) {87
OnExecute();88
NotifyStatusChanged();89
}90

91
IDisposable Members99
}100
101
// Mediator.cs102
internal abstract class Mediator : IDisposable {103
private MainForm mainForm;104
protected IList<Element> elementList;105

106
public Mediator(MainForm mainForm) {107
this.elementList = new List<Element>();108
this.mainForm = mainForm;109
}110

111
~Mediator() {112
Dispose(false);113
}114

115
public MainForm MainForm {116
get {117
return this.mainForm;118
}119
}120

121
public IList<Element> ElementList {122
get {123
return this.elementList;124
}125
}126

127
// Notify all other elements to change status128
public abstract void Notify(Element sorceElement);129

130
// Notify all elements to change status131
public abstract void Notify();132

133
public virtual void AddElement(Element element) {134
// Mainmenu status maybe depended on submenu, So Mainmenu Element should 135
// be placed after submenu Element136
elementList.Insert(0, element);137
}138

139
protected virtual void Dispose(bool disposing) {140
if (disposing) {141
try {142
foreach (Element element in this.elementList) {143
element.Dispose();144
}145
} finally {146
elementList.Clear();147
elementList = null;148
}149
}150
}151

152
IDisposable Members160
} 在这里,MainForm 是主窗口,它充当了中介,它可以被 Element 和 Mediator 引用。
你可能已经发现,Element还依赖于一个 ICommand 接口,IComand 接口引用了一个 object 的对象
在它的一个实现类 MenuItemCommand 中,这个对象指向的是一个 ToolStripMenuItem 的对象。
// ICommand.cs2
public interface ICommand : IDisposable {3
object Source {4
get;5
}6

7
event EventHandler Execute; 8
}9
10
// MenuItemCommand.cs11
internal class MenuItemCommand : ICommand {12
private ToolStripMenuItem source;13
protected EventHandlerList Events;14
private static readonly object EventExecute = new object();15

16
public MenuItemCommand(ToolStripMenuItem source) {17
this.Events = new EventHandlerList();18
this.source = source;19
this.source.Click += SourceOnClick;20
}21

22
~MenuItemCommand() {23
Dispose(false);24
}25

26
private void SourceOnClick(object sender, EventArgs args) {27
OnExecute(EventArgs.Empty);28
}29

30
protected virtual void Dispose(bool disposing) {31
if (disposing) {32
this.source.Click -= SourceOnClick; 33
}34
}35

36
protected virtual void OnExecute(EventArgs args) {37
EventHandler handler = this.Events[EventExecute] as EventHandler;38
if (handler != null) {39
handler(this, args);40
}41
}42

43
ICommand Members61

62
IDisposable Members70
} 那么你可能会问,Element 为什么不直接依赖 ToolStripMenuItem 呢?其实在这个项目里,我们完全
可以这么做,因为我们要使用的仅仅是ToolStripMenuItem, 我们之所以抽象出来一个 ICommand, 完全是
考虑到可能会用到其他的UI组件比如 Button 等,那么如果有这样的需求,很简单,我们可以实现一个
ButtonCommand,让他实现 ICommand 接口,这样保证了接口的统一,它就可以和 ToolStripMenuItem 一起
工作了。简单地说,抽象一个 ICommand 只是为了统一接口。Element 和 Mediator 还实现了 IDispose
接口,这是.Net中被经常提到的 Dispose 模式,如果感兴趣,你可以看看我的另一篇博客:
.Net Dispose 模式 与 C++/CLI 确定性资源清理
实际使用中,Dispose 方法都是通过 Mediator 对象调用的。
好的,抽象已经完成,看看具体怎么实现各个 Menu 的工作细节吧。举一个穿透桌面功能的例子吧:
internal class PenetrateElement : Element {2
public PenetrateElement(Mediator mediator, ToolStripMenuItem source)3
: base(mediator) {4
base.command = new MenuItemCommand(source);5
}6

7
protected override void OnExecute() {8
clockOpt.Penetrate = !clockOpt.Penetrate;9

10
byte alpha = (byte)(mainForm.Opacity * 255);11
if (clockOpt.Penetrate) {12
PenetrateService.MousePenetrate(mainForm, alpha);13
} else {14
PenetrateService.MouseNotPenetrate(mainForm, alpha);15
}16
}17

18
protected internal override void OnStatusChanged() {19
ToolStripMenuItem menuItem = base.Source as ToolStripMenuItem;20
menuItem.Checked = clockOpt.Penetrate;21
}22
} OnExecute 是点击菜单是会执行的方法,OnStatusChanged 的调用可能是因为用户单击了任何一个菜单,
也可能是MainForm中的代码调用了 mediator.Notify(), 由于 Mediator 中引用着所有 Element 的集合,
所以调用 Notify() 将导致所有 Element 的 OnStatusChanged() 被调用,同样,直接点击任何一个菜单项,
也会导致其他(还有自身)菜单接到通知,即 OnStatusChanged() 被调用。所以这些都是在抽象基类里面完成
的,实体 Element 只需要处理这两个方法就可以了。实体 Mediator 本软件中只有一个,他的实现很简单。
// ContextMenuMediator.cs2
internal sealed class ContextMenuMediator : Mediator {3
public ContextMenuMediator(MainForm mainForm)4
: base(mainForm) {5
}6

7
public override void Notify(Element sorceElement) {8
// elementList may be null because it has been disposed9
if (elementList != null) {10
foreach (Element element in elementList) {11
if (!object.ReferenceEquals(element, sorceElement)) {12
element.OnStatusChanged();13
}14
}15
}16
}17

18
public override void Notify() {19
// elementList may be null because it has been disposed20
if (elementList != null) { 21
foreach (Element element in elementList) {22
element.OnStatusChanged();23
}24
}25
}26
}27

为了更好地利用 Visual Studio 的 Designer 设计器,菜单还是在 MainForm 里面用 Designer 生成的,只是在
MainForm 的构造行函数里面,菜单项被 Element 引用。由于进行了设计,功能都被分散了,MainForm 里面的实体
代码并不多,看看吧。
public partial class MainForm : Form, IMementoCapable {2
private ClockOption clockOpt;3
private RemindOperate remindOperate;4
private Mediator mediator;5
private Point mousePosition;6
private Bitmap currentBitmap;7
private WindowShapeMaker windowShapeMaker;8

9
private static readonly int WM_KEYDOWN = 0x0100;10
internal static readonly string AppPath = ClockOption.AppPath;11

12
public MainForm(ClockOption clockOpt) {13
InitializeComponent();14
EnableDoubleBuffering();15
this.clockOpt = clockOpt;16
remindOperate = new RemindOperate();17
Core.Properties properties = PropertyService.Get<Core.Properties>("WinClock.RemindOperate", new Core.Properties());18
remindOperate.SetMemento(properties);19

20
this.mousePosition = Point.Empty;21
this.Cursor = Cursors.SizeAll;22
windowShapeMaker = new WindowShapeMaker(this, clockOpt);23
}24

25
internal ClockOption ClockOption {26
get {27
return this.clockOpt;28
}29
}30

31
internal RemindOperate RemindOperate {32
get {33
return this.remindOperate;34
}35
}36

37
internal void RefreshSkin() {38
string path = Path.Combine(Path.GetDirectoryName(typeof(MainForm).Assembly.Location), "Images");39
string filenaeme = Path.Combine(path, clockOpt.Filename);40

41
try {42
if (!File.Exists(filenaeme)) {43
clockOpt.Filename = "Default.png";44
filenaeme = Path.Combine(path, clockOpt.Filename);45
}46

47
if (currentBitmap != null) {48
currentBitmap.Dispose();49
}50
currentBitmap = new Bitmap(filenaeme);51
RefreshWindow();52
} catch (Exception e) {53
using (EventLog elog = new EventLog()) {54
elog.Log = "Application";55
elog.Source = "WinClock";56
elog.WriteEntry("Error: " + e.Message, EventLogEntryType.Error);57
}58
Environment.FailFast(ResourceService.GetString("CSharpCode.WinClock.MainForm.LackComponent"));59
}60
}61

62
internal void RefreshWindow() {63
windowShapeMaker.RefreshWindow(currentBitmap);64
}65

66
internal void CheckBounds(ref Point location) {67
if (clockOpt.CheckBounds) {68
Rectangle rectScreen = Screen.GetWorkingArea(this);69
if (location.X < rectScreen.Left) {70
location.X = rectScreen.Left;71
} else if (location.X + this.ClientSize.Width > rectScreen.Right) {72
location.X = rectScreen.Right - this.ClientSize.Width;73
}74

75
if (location.Y < rectScreen.Top) {76
location.Y = rectScreen.Top;77
} else if (location.Y + this.ClientSize.Height > rectScreen.Bottom) {78
location.Y = rectScreen.Bottom - this.ClientSize.Height;79
}80
}81
}82

83
protected override void OnLoad(EventArgs e) {84
base.OnLoad(e);85
SetupMenuItemElements();86
mediator.Notify();87
byte alpha = clockOpt.Opacity;88
if (clockOpt.Penetrate) {89
PenetrateService.MousePenetrate(this, alpha);90
} else {91
PenetrateService.MouseNotPenetrate(this, alpha);92
}93
RefreshSkin();94
}95

96
protected override void OnMouseMove(MouseEventArgs e) {97
if (e.Button == MouseButtons.Left) {98
// The clock is fixed up on the desktop99
if (!clockOpt.CanMove)100
return;101

102
int left = this.Location.X + e.Location.X - this.mousePosition.X;103
int top = this.Location.Y + e.Location.Y - this.mousePosition.Y;104
Point location = new Point(left, top);105
CheckBounds(ref location);106
this.SetBounds(location.X, location.Y, this.ClientSize.Width, this.ClientSize.Height);107
clockOpt.Location = this.Location;108
}109

110
base.OnMouseMove(e);111
}112

113
protected override void OnMouseDown(MouseEventArgs e) {114
if (e.Button == MouseButtons.Left) {115
this.mousePosition = e.Location;116
}117

118
base.OnMouseDown(e);119
}120

121
protected override CreateParams CreateParams {122
get {123
CreateParams createParams = base.CreateParams;124
createParams.ExStyle |= PInvokeService.WS_EX_LAYERED;125
return createParams;126
}127
}128

129
protected override void OnMouseLeave(EventArgs e) {130
clockOpt.PreviewOpacity = clockOpt.Opacity;131
windowShapeMaker.RefreshWindow(currentBitmap);132

133
base.OnMouseLeave(e);134
}135

136
protected override void OnMouseEnter(EventArgs e) {137
clockOpt.PreviewOpacity = clockOpt.MouseEnterOpacity;138
windowShapeMaker.RefreshWindow(currentBitmap);139

140
base.OnMouseEnter(e);141
}142

143
// Press ESC to exit144
protected override bool ProcessCmdKey(ref Message msg, Keys keyData) {145
if (msg.Msg == WM_KEYDOWN && keyData == Keys.Escape) {146
this.Close();147
return true;148
}149

150
return base.ProcessCmdKey(ref msg, keyData);151
}152

153
protected override void OnClosed(EventArgs e) {154
base.OnClosed(e);155
Core.Properties properties = this.CreateMemento();156
PropertyService.Set("WinClock.MainForm", properties);157
properties = clockOpt.CreateMemento();158
PropertyService.Set("WinClock.ClockOption", properties);159
}160

161
private void SetupMenuItemElements() {162
mediator = new ContextMenuMediator(this);163
new OptionElement(mediator, menuItemOption);164
new RemindElement(mediator, menuItemRemind);165
new CalendarElement(mediator, menuItemCalendar);166
new AboutElement(mediator, menuItemAbout);167
new PenetrateElement(mediator, menuItemPenetrate);168
new UnmovableElement(mediator, menuItemUnmovable);169
new TopMostElement(mediator, menuItemTopMost);170
new MinimizeElement(mediator, menuItemMiniMize);171
new ExitElement(mediator, menuItemExit);172

173
// Additional function174
new NotepadElement(mediator, menuItemNotePad);175
new CalcElement(mediator, menuItemCalc);176
new MediaPlayerElement(mediator, menuItemMediaPlayer);177
new BackupElement(mediator, menuItemBackup);178
new DirectXElement(mediator, menuItemDirectx);179
new SystemDoctorElement(mediator, menuItemDoctor);180

181
// Shutdown windows182
new ShutdownElement(mediator, menuItemShutDown);183

184
// Language185
new LanguageElement(mediator, menuItemLanguage);186
}187

188
private void NotifyIconOnDoubleClick(object sender, EventArgs e) {189
this.Activate();190
}191

192
private void NotifyIconOnMouseDown(object sender, MouseEventArgs e) {193
// Right click to active the window194
if (e.Button == MouseButtons.Right) {195
this.Activate();196
}197
}198

199
private void NotifyIconOnMouseMove(object sender, MouseEventArgs e) {200
// Show the tooltip201
string time = DateTime.Now.ToString(Core.RemindOperate.YearMonthDayTimeFmt);202
string week = Core.RemindOperate.Days[DateTime.Now.DayOfWeek];203
if (string.Equals("zh-CHS", clockOpt.Language)) {204
string lunar = ChineaseDateService.GetLunarCalendar(DateTime.Now.Date);205
notifyIcon.Text = string.Format("{1} {2}{0}农历{3}", Environment.NewLine, time, week, lunar);206
} else {207
notifyIcon.Text = string.Format("{0} {1}", time, week);208
}209
}210

211
private void TimerSecondOnTick(object sender, EventArgs e) {212
windowShapeMaker.RefreshWindow(currentBitmap);213
}214

215
private void TimerRemindOnTick(object sender, EventArgs e) {216
remindOperate.CheckRemindList();217
}218

219
private void EnableDoubleBuffering() {220
this.SetStyle(ControlStyles.OptimizedDoubleBuffer &221
ControlStyles.UserPaint &222
ControlStyles.AllPaintingInWmPaint,223
true);224

225
this.UpdateStyles();226
}227
228
protected override void Dispose(bool disposing) {229
if (disposing) {230
IDisposable[] resourceList = new IDisposable[] {231
mediator,232
currentBitmap,233
windowShapeMaker, 234
components235
};236
foreach (IDisposable resource in resourceList) {237
if (resource != null) {238
resource.Dispose();239
}240
}241
}242
base.Dispose(disposing);243
}244

245
IMementoCapable Members265
} 这里 Designer 生成的代码没有列出来,其中 SetupMenuItemElements 方法就是完成了实例化 Element 和 Mediator
具体类的作用, 这样他们都被 medator 引用,所以你不用担心它马上会被垃圾收集干掉。
参考资料:
关机对话框
李建忠-C#面向对象模式纵横谈第七讲-(行为型模式) Mediator 中介者模式

浙公网安备 33010602011771号