WPF一步步开发XMPP IM客户端1:入门
[起因&目标]
因为工作原因接触openfire服务端和spark客户端开发,主要是基于openfire扩展开发了针对企业用途的服务器插件,还开发了各个平台上的客户端(Windows\mac\android\ios\linux),详情可搜索微信公众号:CVTalk
在开发过程中,发现基于Spark开发Java Swing客户端(公司内部命名CVTalk)比较重,用户体验很难做到和微信客户端看齐,皮肤的开发也比较费力,公司里绝大部分是windows的客户端,而且高清分辨率屏幕的用户越来越多, 那蚂蚁一样的小字和拥挤的布局改造很费劲,内存占用过多的问题也解决不了,swing客户端越发力不从心。
于是,我打算利用业余时间做一个 轻量级、易用、稳定、美观的Windows客户端。并抛砖引玉放开源:https://lightchat.codeplex.com/
坚持轻客户端,体验第一。 目标功能只有7个
1、 单聊
2、 群聊
3、 人员搜索
4、 消息搜索
5、 截屏传输
6、 文件传输
7、 插件扩展平台和企业业务集成接口,及xmpp少量扩展和服务器插件
特性:
- XMPP message、iq、presence、vcard、roster.....
- MUC(Multi-User Chat) support
- Fast localized(sqlite) user search,and Openfire ofUser import & sync
- Easy message search console
- Extensible organizational structure tree interface (Chinese users love it)
- HD resolution screen support
- ...
界面采用Modern UI。布局采用微信PC的方式,即搜索+联系人+聊天窗一体,不开新窗。
第一版的界面,单聊界面
消息搜索:
[项目介绍]
项目目录介绍:
主要类介绍
1、Config.cs:全局设置类
//是否显示即时调试窗口 public static bool IsDebug = false; public static String Version = "0.1"; //@加上bareJid的域名部分 public static String Domain = "@im"; //注意:这个是域名,一般为该服务器的机器名 public static String Server = "im";//外网或内网IP,目前AutoResolveConnectServer设置为true也不起作用,只能先改hosts文件 //public static readonly String ServerIP = "111.63.127.83"; public static readonly String ResourceName = "LightChat"; public static readonly bool IsCheckPresence = false; public static readonly bool IsCheckChatState = false; public static readonly bool AutoAgents = false; public static readonly bool AutoPresence = true; public static readonly bool AutoRoster = true; //### 如果无法登陆,请在hosts文件中关联IP到Server域名 ###//C:\Windows\System32\drivers\etc\hosts 最后一行增加IP映射 比如 111.63.127.83 im //自动解析connectserver属性,设置为true就会解析server属性即会利用System.Net.DNS.Resolve方法来将域名映射成ip地址 public static readonly bool AutoResolveConnectServer = false; public static String MeCharacter = "我: "; //### Organization structure data source definition ### //### 组织架构数据源定义 ### //support:XML\Json\SqliteDB //public static IDataSource OrgSource = new XmlDataSource(); public static IDataSource OrgSource = new DbDataSource(); //### User search data source ### //### 用户搜索数据源 ### public static IResultsProvider SearchSource = new UserSearchProvider();
用户搜索数据源,使用统一的返回结果接口IResultsProvider
public static IResultsProvider SearchSource = new UserSearchProvider();
使用接口的目的是使业务代码具有扩展性,比如组织架构数据源实现松耦合的具体实现。
2、登陆类: LightChat.Pages.Login
代码
/// <summary> /// Login.xaml 的交互逻辑 /// </summary> public partial class Login : UserControl { public Login() { InitializeComponent(); //读取设置 this.Dispatcher.BeginInvoke(new Action(LoadSettings), null); } private void btnLogin_Click(object sender, RoutedEventArgs e) { LoginTalk(); } private void LoginTalk() { this.Dispatcher.BeginInvoke(new Action(BusyRun), null); if (tbxUsername.Text.Trim() == "") { ModernProgressRing mpr = new ModernProgressRing(); ModernDialog.ShowMessage("注意:您的账户名未填写。", "提示",MessageBoxButton.OK); return; } if (tbxPassword.Password == "") { ModernDialog.ShowMessage("注意:您的账户名未填写。", "提示", MessageBoxButton.OK); return; } LoginXMPP(); SaveSettingsDB(); } /// <summary> /// 执行登录 /// </summary> private void LoginXMPP() { XmppStatic.xmppCon = new XmppClientConnection(); XmppStatic.xmppCon.Server = Config.Server; XmppStatic.xmppCon.Username = tbxUsername.Text.Trim(); XmppStatic.xmppCon.Password = tbxPassword.Password; XmppStatic.xmppCon.Resource = Config.ResourceName; XmppStatic.xmppCon.Priority = 0; XmppStatic.xmppCon.AutoAgents = Config.AutoAgents; XmppStatic.xmppCon.AutoPresence = Config.AutoPresence; XmppStatic.xmppCon.AutoRoster = Config.AutoRoster; XmppStatic.xmppCon.AutoResolveConnectServer = Config.AutoResolveConnectServer; XmppStatic.xmppCon.UseStartTLS = true; XmppStatic.xmppCon.ClientVersion = "1.0"; XmppStatic.xmppCon.Capabilities.Node = "http://www.cvtalk.cn/caps"; SetDiscoInfo(); try { //登录事件,异步 XmppStatic.xmppCon.OnLogin += new ObjectHandler(xmppCon_OnLogin); //socket断开事件 XmppStatic.xmppCon.OnClose += new ObjectHandler(xmppCon_OnClose); //连接XMPP服务 XmppStatic.xmppCon.Open(); XmppStatic.SelfJID = new Jid(tbxUsername.Text.Trim() + Config.Domain + "/"+ Config.ResourceName); } catch (Exception ex) { Console.WriteLine(ex.Message); } } private void xmppCon_OnClose(object sender) { this.Dispatcher.BeginInvoke(new Action(UIChangeOnClose), null); } /// <summary> /// 断线后的UI改变,需要后续开发断线重连 /// </summary> private void UIChangeOnClose() { UIStatic.mainHome.SetLogText("连接关闭"); //todo : 自动重连逻辑... } /// <summary> /// 登录后的UI改变 /// </summary> /// <param name="sender"></param> void xmppCon_OnLogin(object sender) { this.Dispatcher.BeginInvoke(new Action(UIChangeOnLogin), null); } private void UIChangeBeforeLogin() { } private void UIChangeOnLogin() { tbxUsername.IsEnabled = false; tbxPassword.IsEnabled = false; btnLogin.IsEnabled = false; //设置主窗体的Tab展示 UIStatic.mainWindow.SetBussTabText("Work"); UIStatic.mainWindow.SetHomeTabText("Chat"); UIStatic.mainWindow.SetHomeTabText(); UIStatic.mainWindow.ContentSource = new System.Uri("/Pages/Home.xaml", UriKind.Relative); BusyStop(); //VersionIq viq = new VersionIq(IqType.get, new Jid("wangxin@im/PC"), new Jid("zz@im")); //xmppCon.Send(viq); //DiscoManager dmg = new DiscoManager(xmppCon); //dmg.DiscoverInformation(new Jid("im")); //LastIq lastIq = new LastIq(IqType.get, new Jid("wangxin@im/PC")); //XmppStatic.xmppCon.Send(lastIq); PrivateIq privateIq = new PrivateIq(IqType.get); privateIq.Query.Storage = new agsXMPP.protocol.extensions.bookmarks.Storage(); XmppStatic.xmppCon.IqGrabber.SendIq(privateIq, new IqCB(PrivateIqResult), null); ///SearchIq searchIq = new SearchIq(IqType.set, new Jid("search.im")); //searchIq.Query.Email = "wangx"; //xmppCon.Send(searchIq); //VcardIq vcardIq = new VcardIq(); //vcardIq.Type = IqType.get; //vcardIq.To = new Jid("wangxin@im"); //XmppStatic.xmppCon.IqGrabber.SendIq(vcardIq, new IqCB(VcardIqResult), null); //BookmarkManager bkmg = new BookmarkManager(XmppStatic.xmppCon); //bkmg.RequestBookmarks(); } private void VcardIqResult(object sender, IQ iq, object data) { if (iq.Type == IqType.result) { Vcard vcard = iq.Vcard; if (vcard != null) { string fullname = vcard.Fullname; string nickname = vcard.Nickname; string description = vcard.Description; Photo photo = vcard.Photo; } } } //获取MUC会议室书签Bookmarks private void PrivateIqResult(object sender, IQ iq, object data) { if (iq.Type == IqType.result) { Private piq = iq.Query as Private; if (piq != null) { if (piq.HasChildElements) { XmppStatic.Conferences = ((Storage)piq.FirstChild).GetConferences(); } } } } /// <summary> /// 设置本地客户端XMPP发现服务 /// </summary> private void SetDiscoInfo() { XmppStatic.xmppCon.DiscoInfo.AddIdentity(new DiscoIdentity("pc", "cvtalk", "client")); XmppStatic.xmppCon.DiscoInfo.AddFeature(new DiscoFeature(agsXMPP.Uri.DISCO_INFO)); XmppStatic.xmppCon.DiscoInfo.AddFeature(new DiscoFeature(agsXMPP.Uri.DISCO_ITEMS)); XmppStatic.xmppCon.DiscoInfo.AddFeature(new DiscoFeature(agsXMPP.Uri.MUC)); // for testing to bypass disco caches //_connection.DiscoInfo.AddFeature(new DiscoFeature(Guid.NewGuid().ToString())); } private void LoadSettings() { //建立数据库 CreateSaveTable.InitDB(); //读取设置,用户记住用户名、密码等 DataTable dtSelect = LoadTable.LoadSettings(); if (dtSelect == null || dtSelect.Rows.Count < 1) //没有数据 { chkRememberPSW.IsChecked = true; } else { tbxUsername.Text = dtSelect.Rows[0]["Jid"] + ""; tbxPassword.Password = dtSelect.Rows[0]["Password"] + ""; chkRememberPSW.IsChecked = dtSelect.Rows[0]["RememberPSW"] + "" == "true" ? true : false; chkAutomatic.IsChecked = dtSelect.Rows[0]["Automatic"] + "" == "true" ? true : false; } } /// <summary> /// 保存本地设置 /// </summary> private void SaveSettingsDB() { CreateSaveTable.SaveSettingsDB(tbxUsername.Text, tbxPassword.Password, (bool)chkRememberPSW.IsChecked, (bool)chkAutomatic.IsChecked); } private void tbxPassword_KeyDown(object sender, KeyEventArgs e) { if (e.Key == Key.Enter) { //enter key is down LoginTalk(); } } private void BusyRun() { //busyr.IsBusy = true; this._loading.Visibility = Visibility.Visible; } private void BusyStop() { //busyr.IsBusy = false; this._loading.Visibility = Visibility.Collapsed; } }
3、主窗体: LightChat.Pages.Home
区域介绍
代码下载地址:https://lightchat.codeplex.com/
下一篇预告:WPF一步步开发XMPP IM客户端:主窗体设计
目前维护的开源产品:https://gitee.com/475660