WebSocketで実現する同期するUI。group1とgroup2のコントロールの値を同期させてみる。
なんか思いつきで作ってみた。
サーバはPython,画面はC#。
クラスライブラリ化したので部品として使用したい場合は、さくっと使用出来る。
staticクラスを使用してWebSocketの接続部を作成。ソケットの部分はシングルトンイメージ。
WsExtensionsクラス
/// <summary> /// Contolの拡張メソッドと静的変数の定義 /// </summary> public static class WsExtensions { /// <summary> /// WebSocket接続用 /// </summary> private static WebSocket _ws = null; /// <summary> /// WebSocketでの受信時イベントハンドラ /// </summary> public static EventHandler OnData; /// <summary> /// 受信イベント処理 /// </summary> /// <param name="ctrl"></param> /// <param name="e"></param> private static void OnRcvHandler(Control ctrl, WebSocketEventArgs e) { if (OnData != null) { OnData(ctrl, e); } } /// <summary> /// コントロールに紐つけるIDをセットする /// </summary> /// <param name="ctrl"></param> /// <param name="name"></param> public static void WSSetUIName(Control ctrl, string name) { ctrl.Tag = name; } /// <summary> /// WebSocket初期化 /// </summary> /// <param name="ctrl"></param> /// <param name="wsuri"></param> /// <param name="uiname"></param> public static void WSConnect(System.Windows.Forms.Control ctrl, string wsuri, string uiname) { /// 対象のControl(パラメータ ctrl)とIDをセットする WSSetUIName(ctrl, uiname); /// WebSocket初期化 if (_ws == null) { _ws = new WebSocket(wsuri); _ws.Open(); _ws.OnData += (s, e) => { OnRcvHandler(ctrl, e); }; } ctrl.Disposed += (s, e) => { if ((_ws != null) && (_ws.ReadyState != WebSocketState.Closed)) { _ws.Close(); } }; } /// <summary> /// WebSocket送信処理 /// </summary> /// <param name="ctrl"></param> /// <param name="value"></param> public static void SendMessage(Control ctrl, byte[] value) { /// IDとデータをパックして送信 var uimessage = new UIMessage(); uimessage._0UIName = (string)ctrl.Tag; uimessage._1Value = value; _ws.SendMessage(uimessage.Object2MessagePack()); } /// <summary> /// object -> MessagePack(byte[]) /// </summary> /// <typeparam name="T"></typeparam> /// <param name="obj"></param> /// <returns></returns> public static byte[] Object2MessagePack<T>(this object obj) { var serializer = MessagePackSerializer.Create<T>(); var ms = new System.IO.MemoryStream(); serializer.Pack(ms, (T)obj); var ret = ms.ToArray(); ms.Close(); return ret; } public static byte[] Object2MessagePack(this object obj) { var serializer = MessagePackSerializer.Create<object>(); var ms = new System.IO.MemoryStream(); serializer.Pack(ms, (object)obj); var ret = ms.ToArray(); ms.Close(); return ret; } /// <summary> /// MessagePack(byte[]) -> object /// </summary> /// <typeparam name="T"></typeparam> /// <param name="bytes"></param> /// <returns></returns> public static object MessagePack2Object<T>(this byte[] bytes) { var serialize = MessagePackSerializer.Create<T>(); var ms = new System.IO.MemoryStream(bytes); var ret = serialize.Unpack(ms); ms.Close(); return ret; } /// <summary> /// 送信用データクラス /// </summary> public class UIMessage { /// <summary> /// 対象となるControlに割り振ったIDを指定する /// </summary> public string _0UIName { get; set; } /// <summary> /// 値 /// </summary> public byte[] _1Value { get; set; } } }
UIにより値を格納している方法が違うので、元のコントロールクラス(System.Windows.Form)を継承した各コントロールクラスを作成。そして、値が変更になった場合にデータをソケットで送信し、データが受信された場合にはコントロールの値を変更する。
UIクラス
public class UI { /// <summary> /// TextBox /// </summary> public class wsTextBox : System.Windows.Forms.TextBox { public wsTextBox() { WsExtensions.OnData += (s,e)=> { var rcvData = (WebSocketEventArgs)e; var uiMes = (WsExtensions.UIMessage)rcvData.BinaryData.MessagePack2Object<WsExtensions.UIMessage>(); if (uiMes._0UIName != this.Tag.ToString()) return; if (!this.Focused) { this.Text = (string)uiMes._1Value.MessagePack2Object<string>(); } }; this.TextChanged += (s, e) => { if (this.Focused) WsExtensions.SendMessage(this,this.Text.Object2MessagePack()); }; } } /// <summary> /// HScrollBar /// </summary> public class wsHScrollBar : System.Windows.Forms.HScrollBar { public wsHScrollBar() { bool flg = false; WsExtensions.OnData += (s, e) => { var rcvData = (WebSocketEventArgs)e; var uiMes = (WsExtensions.UIMessage)rcvData.BinaryData.MessagePack2Object<WsExtensions.UIMessage>(); if (uiMes._0UIName != this.Tag.ToString()) return; if (!flg) { this.Value = (int)uiMes._1Value.MessagePack2Object<int>(); } }; this.MouseHover += (s, e) => flg = true; this.MouseLeave += (s, e) => flg = false; this.ValueChanged += (s, e) => { if (flg) WsExtensions.SendMessage(this, this.Value.Object2MessagePack()); }; } } /// <summary> /// VScrollBar /// </summary> public class wsVScrollBar : System.Windows.Forms.VScrollBar { public wsVScrollBar() { bool flg = false; WsExtensions.OnData += (s, e) => { var rcvData = (WebSocketEventArgs)e; var uiMes = (WsExtensions.UIMessage)rcvData.BinaryData.MessagePack2Object<WsExtensions.UIMessage>(); if (uiMes._0UIName != this.Tag.ToString()) return; if (!flg) { this.Value = (int)uiMes._1Value.MessagePack2Object<int>(); } }; this.MouseHover += (s, e) => flg = true; this.MouseLeave += (s, e) => flg = false; this.ValueChanged += (s, e) => { if (flg) WsExtensions.SendMessage(this, this.Value.Object2MessagePack()); }; } } /// <summary> /// Label(受信専用) /// </summary> public class wsOptInLabel : System.Windows.Forms.Label { public wsOptInLabel() { WsExtensions.OnData += (s, e) => { var rcvData = (WebSocketEventArgs)e; var uiMes = (WsExtensions.UIMessage)rcvData.BinaryData.MessagePack2Object<WsExtensions.UIMessage>(); if (uiMes._0UIName != this.Tag.ToString()) return; this.Text = (string)uiMes._1Value.MessagePack2Object<string>(); }; } } /// <summary> /// CheckBox /// </summary> public class wsCheckBox : System.Windows.Forms.CheckBox { public wsCheckBox() { WsExtensions.OnData += (s, e) => { var rcvData = (WebSocketEventArgs)e; var uiMes = (WsExtensions.UIMessage)rcvData.BinaryData.MessagePack2Object<WsExtensions.UIMessage>(); if (uiMes._0UIName != this.Tag.ToString()) return; this.Checked = (bool)uiMes._1Value.MessagePack2Object<bool>(); }; this.CheckedChanged += (s, e) => { WsExtensions.SendMessage(this, this.Checked.Object2MessagePack()); }; } } /// <summary> /// RadioButton /// </summary> public class wsRadioButton : System.Windows.Forms.RadioButton { public wsRadioButton() { WsExtensions.OnData += (s, e) => { var rcvData = (WebSocketEventArgs)e; var uiMes = (WsExtensions.UIMessage)rcvData.BinaryData.MessagePack2Object<WsExtensions.UIMessage>(); if (uiMes._0UIName != this.Tag.ToString()) return; this.Checked = (bool)uiMes._1Value.MessagePack2Object<bool>(); }; this.CheckedChanged += (s, e) => { WsExtensions.SendMessage(this, this.Checked.Object2MessagePack()); }; } } }
同期用のGUIコントロール(WebSocketUILib.UI)はすべてSystem.Windows.Formsからの派生型。
デザイナで配置したあと、親FormのDesigner.csでInitializeComponent()と変数定義を書き換える。
/// <summary> /// デザイナー サポートに必要なメソッドです。このメソッドの内容を /// コード エディターで変更しないでください。 /// </summary> private void InitializeComponent() { this.textBox1 = new WebSocketUILib.UI.wsTextBox(); this.textBox2 = new WebSocketUILib.UI.wsTextBox(); this.groupBox1 = new System.Windows.Forms.GroupBox(); this.checkBox2 = new WebSocketUILib.UI.wsCheckBox(); this.checkBox1 = new WebSocketUILib.UI.wsCheckBox(); this.groupBox2 = new System.Windows.Forms.GroupBox(); this.radioButton2 = new WebSocketUILib.UI.wsRadioButton(); this.radioButton1 = new WebSocketUILib.UI.wsRadioButton(); this.vScrollBar1 = new WebSocketUILib.UI.wsVScrollBar(); this.label2 = new WebSocketUILib.UI.wsOptInLabel(); this.label1 = new WebSocketUILib.UI.wsOptInLabel(); this.hScrollBar1 = new WebSocketUILib.UI.wsHScrollBar(); this.groupBox1.SuspendLayout(); this.groupBox2.SuspendLayout(); this.SuspendLayout(); /// 省略 } /// 変数定義の書き換え private WebSocketUILib.UI.wsTextBox textBox1; private WebSocketUILib.UI.wsTextBox textBox2; private System.Windows.Forms.GroupBox groupBox1; private WebSocketUILib.UI.wsCheckBox checkBox2; private WebSocketUILib.UI.wsCheckBox checkBox1; private System.Windows.Forms.GroupBox groupBox2; private WebSocketUILib.UI.wsRadioButton radioButton2; private WebSocketUILib.UI.wsRadioButton radioButton1; private WebSocketUILib.UI.wsVScrollBar vScrollBar1; private WebSocketUILib.UI.wsOptInLabel label2; private WebSocketUILib.UI.wsOptInLabel label1; private WebSocketUILib.UI.wsHScrollBar hScrollBar1; }
親Formのコンストラクタで同期対象コントロールのWebSocketを初期化する。全部のUIで初期化しているけど、実際には1接続しか確立しないようになっている。
親Formのコンストラクタ
public Form1() { InitializeComponent(); // group1 WsExtensions.WSConnect(textBox1,"ws://192.168.1.22:12345/ui", "Text1"); /// -> label1と同期 WsExtensions.WSConnect(textBox2,"ws://192.168.1.22:12345/ui", "Text2"); /// -> label2と同期 WsExtensions.WSConnect(hScrollBar1,"ws://192.168.1.22:12345/ui", "BarValue"); /// -> vScrollBar1と同期 hScrollBar1.Minimum = 0; hScrollBar1.Maximum = 100; WsExtensions.WSConnect(checkBox1,"ws://192.168.1.22:12345/ui", "Check1"); /// -> radioButton1と同期 WsExtensions.WSConnect(checkBox2,"ws://192.168.1.22:12345/ui", "Check2"); /// -> radioButton2と同期 // group2 WsExtensions.WSConnect(label1,"ws://192.168.1.22:12345/ui", "Text1"); /// -> textBox1と同期 WsExtensions.WSConnect(label2,"ws://192.168.1.22:12345/ui", "Text2"); /// -> textBox2と同期 WsExtensions.WSConnect(vScrollBar1,"ws://192.168.1.22:12345/ui", "BarValue"); /// -> hScrollBar1と同期 vScrollBar1.Minimum = 0; vScrollBar1.Maximum = 100; WsExtensions.WSConnect(radioButton1,"ws://192.168.1.22:12345/ui", "Check1"); /// -> checkBox1と同期 WsExtensions.WSConnect(radioButton2,"ws://192.168.1.22:12345/ui", "Check2"); /// -> checkBox2と同期 }
ポイントはWSConnectで渡している2つめのパラメータ。これを定義することで、同じIDが定義された他のコントロールともデータの共有ができる。
UIクラスの各コントロールソース内で、受信データとして送られてくるIDと自分自身の値(ControlクラスのTagプロパティ)を比較し、受信すべきデータであるか判定している。C#は多重継承ができないのでControlクラスがもっているTag(object型)は助かった。
今回は値だけだったけど、他のプロパティを変更するコードを書けばもっと面白くなるかもしれない。テキストボックスやスライドバーなどは他の言語でもよくある型なので、連携させるのはあっさりできるかもしれない。
ちなみに、以下サーバソース。サーバはgeventwebsocketのブロードキャストサーバのほぼまんま。
ui_server.py
#!/usr/bin/env python2.6 # -*- coding:utf-8 -*- import os import geventwebsocket import msgpack from gevent import pywsgi from geventwebsocket.handler import WebSocketHandler websocketCons = [] def connection_handle(wenv): ws = wenv['wsgi.websocket'] wskey = wenv['HTTP_SEC_WEBSOCKET_KEY'] websocketCons.append(ws) print "add memlen = " + str(len(websocketCons)) while True: msg = ws.receive() if msg is None: break i = 0 while i < len(websocketCons): try: websocketCons[i].send(msg) except: websocketCons.remove(websocketCons[i]) i += 1 print "remove" websocketCons.remove(ws) def websocket_app(environ, response): path = environ["PATH_INFO"] res_ConText = [("Content-Type", "text/html")] if path == "/": response("200 OK", res_ConText) return open('./index.html').read() elif path == "/ui": connection_handle(environ) else: response("404 Not Found",res_ConText) return "======= Not Found! =======" if __name__ == "__main__": server = pywsgi.WSGIServer(('192.168.1.22', 12345), websocket_app, handler_class=WebSocketHandler) server.serve_forever()
今回作成したソースはこちら。