之前看过有用WPF+WCF实现共享白板的示例(CodeProject:http://www.codeproject.com/KB/WCF/DrawMeWCF.aspx) ,最近闲来无事重复造个Winform的轮子。在那篇文章里,利用了WPF的 InkCanvas Control + WCF Duplex 实现了某个客户端画图提交到服务端,服务端负责通知其他客户端。
先说说我的思路:服务端画图,客户端通过Timer轮询获得服务端画图信息,重画到自己的窗体上,使得客户端看上去像是在和服务端同步画图。先上张效果图:左边是Server,右边是Client。
当然这样有个不足,就是只能在服务端画图客户端收看。
工程很简单,窗体+WCF Library,窗体是服务端和客户端共用的,程序login的时候区分是服务端还是客户端:
public partial class Login : Form { public Login() { InitializeComponent(); } private void btnLogin_Click(object sender, EventArgs e) { Main main = Application.OpenForms["Main"] as Main; if (radServer.Checked) { main.IsServer = true; this.Close(); } else if (radClient.Checked) { main.IsServer = false; this.Close(); } else MessageBox.Show(this, "Please select the type."); } private void btnCancel_Click(object sender, EventArgs e) { Application.Exit(); } }
选择服务端启动时,则将WCF Service Host到当前的Windows Form应用上。关于Winform Host,需要注意把ServiceHost封装到线程里,避免UI阻塞。(参看我的另一篇博客:WCF常见问题(2) -- Winform Host UI阻塞)
Text += "[Server]"; Host = new ThreadedServiceHost(typeof(WcfShareDrawLib.DrawService)); Host.Open();
选择客户端启动时,通过WCF Data Contract(Interface)创建远程代理,因为共用Winform工程,App.config也是共用的,因此利用ConfigurationManager读取ServiceModel配置里的服务地址,避免了硬编码写死地址。
#region get info from config var conf = ConfigurationManager.OpenExeConfiguration(Assembly.GetEntryAssembly().Location); var svcConf = (ServiceModelSectionGroup)conf.GetSectionGroup("system.serviceModel"); var address = ""; foreach (ServiceElement svc in svcConf.Services.Services) address = svc.Host.BaseAddresses[0].BaseAddress; #endregion var binding = new NetTcpBinding(); binding.CloseTimeout = new TimeSpan(0, 10, 0); binding.ReceiveTimeout = new TimeSpan(0, 10, 0); binding.SendTimeout = new TimeSpan(0, 10, 0); var factory = new ChannelFactory<IDrawService>(binding, address); factory.Open(); Client = factory.CreateChannel();
完整的MainForm代码:
public partial class Main : Form { public Main() { InitializeComponent(); } private DrawCtrl _draw; public bool IsServer { get; set; } public ThreadedServiceHost Host { get; set; } public IDrawService Client { get; set; } private void Form1_Load(object sender, EventArgs e) { _draw = new DrawCtrl(); this.SetStyle(ControlStyles.OptimizedDoubleBuffer | ControlStyles.ResizeRedraw | ControlStyles.AllPaintingInWmPaint, true); } private void Main_Shown(object sender, EventArgs e) { var login = new Login(); login.StartPosition = FormStartPosition.CenterParent; login.ShowDialog(this); if (IsServer) SetServer(); else SetClient(); } private void Main_FormClosing(object sender, FormClosingEventArgs e) { timer1.Stop(); if (Client != null) ((IDisposable)Client).Dispose(); if (Host != null) ((IDisposable)Host).Dispose(); _draw.Dispose(); } private void SetServer() { Text += "[Server]"; Host = new ThreadedServiceHost(typeof(WcfShareDrawLib.DrawService)); Host.Open(); DrawService.OnRequestData += () => { return _draw.Lines; }; panel1.Paint += (s, evt) => { _draw.DrawAll(evt.Graphics); }; panel1.MouseDown += (s, evt) => { _draw.Begin(); }; panel1.MouseMove += (s, evt) => { _draw.Draw(panel1.CreateGraphics(), evt.Location); }; panel1.MouseUp += (s, evt) => { _draw.DrawTemp(panel1.CreateGraphics()); _draw.Stop(); }; btnRedraw.Click += (s, evt) => { _draw.Clear(panel1.CreateGraphics(), panel1.BackColor); }; } private void SetClient() { Text += "[Client]"; #region get info from config var conf = ConfigurationManager.OpenExeConfiguration(Assembly.GetEntryAssembly().Location); var svcConf = (ServiceModelSectionGroup)conf.GetSectionGroup("system.serviceModel"); var address = ""; foreach (ServiceElement svc in svcConf.Services.Services) address = svc.Host.BaseAddresses[0].BaseAddress; #endregion var binding = new NetTcpBinding(); binding.CloseTimeout = new TimeSpan(0, 10, 0); binding.ReceiveTimeout = new TimeSpan(0, 10, 0); binding.SendTimeout = new TimeSpan(0, 10, 0); var factory = new ChannelFactory<IDrawService>(binding, address); factory.Open(); Client = factory.CreateChannel(); timer1.Interval = 1000; timer1.Start(); timer1.Tick += (s, evt) => { try { var lines = Client.GetDrawData(); if (lines.Count > 1) _draw.DrawAll(panel1.CreateGraphics(), lines); else _draw.Clear(panel1.CreateGraphics(), panel1.BackColor); } catch (Exception) { } }; } }
可以看到客户端起动一个Timer,定时向服务索取画图信息:
var lines = Client.GetDrawData();
服务端的GetDrawData()方法并调用时,将触发一个static事件:OnRequestData。服务端UI注册了这个事件的处理方法,将当前的图片信息返回。简单的过程如下图:
WCF Service的代码:
[ServiceBehavior(InstanceContextMode=InstanceContextMode.Single, ConcurrencyMode=ConcurrencyMode.Multiple)] public class DrawService : IDrawService { public static event Func<List<List<Point>>> OnRequestData; public List<List<Point>> GetDrawData() { return OnRequestData(); } }
当然,我们还需要一个画图的控制类:
(在上面UI的代码中,SetService方法里在各个UI事件里调用了下面的方法)
1. 鼠标按下时,开始
2. 鼠标按下移动时,记录坐标并画当前的线
3. 鼠标松开时,结束(重画当前画的这一条线,否则会有断点显得线条不平滑)
4. 在Paint事件里,重画所有线条
public class DrawCtrl : IDisposable { private List<List<Point>> _lines; private List<List<Point>> _tempLines; private List<Point> _newLine; private bool _isDrawing; private Pen _pen; private int _redrawNum; public List<List<Point>> Lines { get { lock (this) { return _lines; } } } public DrawCtrl() { _lines = new List<List<Point>>(); _tempLines = new List<List<Point>>(); _isDrawing = false; _pen = new Pen(Brushes.Black, 5); } public void Begin() { _isDrawing = true; _newLine = new List<Point>(); _tempLines.Clear(); _lines.Add(_newLine); _tempLines.Add(_newLine); _redrawNum = 0; } public void Stop() { _isDrawing = false; } public void Draw(Graphics g, Point p) { if (!_isDrawing) return; lock (this) { _newLine.Add(p); var count = _newLine.Count; if (count > 1) { g.DrawLine(_pen, _newLine[count - 2], _newLine[count - 1]); g.Dispose(); } } } public void Clear(Graphics g, Color color) { lock (this) { _lines.Clear(); } g.Clear(color); } public void DrawAll(Graphics g) { DrawAll(g, _lines); } public void DrawTemp(Graphics g) { DrawAll(g, _tempLines); } public void DrawAll(Graphics g, List<List<Point>> lines) { lock (this) { lines.ForEach(line => { if (line.Count > 1) g.DrawLines(_pen, line.ToArray()); }); g.Dispose(); } } public void Dispose() { _lines.ForEach(l => l.Clear()); _lines.Clear(); _pen.Dispose(); } }
OK,一个简单的白板共享的程序就搞定了。