本文經原作者授權以原創方式二次分享,歡迎轉載、分享。
原文作者:眾尋
原文連結:https://www.cnblogs.com/ZXdeveloper/p/6058206.html
公司的同事離職了,接下來的日子可能會忙碌,能完善 demo 的時間也會少了,因此,把做的簡易 demo 整體先記錄一下,等後續不斷的完善。
参考两位大神的日志:WEB 版微信协议部分功能分析、【完全开源】微信客户端.NET 版
尤其是周见智大神的 DEMO,因为好多和微信的服务端交互,都借鉴了大神的源码,帮助巨大,可以说我相当于做了一个翻版,只是用 WPF 开发的而已,外观上不同,但是实际交互上是差不多的。
微信分為兩個部分,一個是登錄,一個是主體,基於此,wpf 也主要是這兩個窗體來實現。
一、登錄模塊
1、登錄部分分為二維碼和獲取用戶頭像兩個頁面(因為是給予 web 的,所以沒有客戶端的登錄按鈕,只能通過掃碼來登錄)


在程式啟動以後,先通過請求獲取到二維碼,然後,在啟動一個新的線程,不斷的循環檢索登錄狀態。
private void LoopLoginCheck()
{
object login_result = null;
//循环判断手机扫描二维码结果
while (true)
{
login_result = ls.LoginCheck();
//已扫描 未登录
if (login_result is ImageSource)
{
HeadImageSource = login_result as ImageSource;
//广播,通知到LoginUC页面,切换
Messenger.Default.Send<object>(null, "ShowLoginInfoUC");
}
//已完成登录
if (login_result is string)
{
//访问登录跳转URL
ls.GetSidUid(login_result as string);
//广播,隐藏登录页面,打开主页面
Messenger.Default.Send<object>(null, "HideLoginUC");
thread.Abort();
break;
}
////超时
if (login_result is int)
{
//QRCodeImageSource = ls.GetQRCode();
//返回二维码页面
Messenger.Default.Send<object>(null, "ShowQRCodeUC");
}
}
}
因為是 mvvm,所以,需要用廣播來進行操作頁面的切換,即填充到登錄窗體中間的控制項是二維碼,還是頭像。
2、大家可以看到我上面的截圖部分包含了一部分的背景,這個是用 snagit(推薦這個截圖工具,很好用)截圖時,自動截出的,因為窗體本身的大小就是那麼大,多餘出來的部分是透明的,用來做二維碼滑動出現的效果部分。

当处于二维码状态时划过,则出现动画,头像状态下则没有动画,是设置了 Image 的 Visibility 属性来控制的,滑动效果可以看我的另一篇博客微信 二维码鼠标滑动 图像显隐效果。
3、當掃碼成功,並且在手機端點擊登錄以後,則跳轉到主頁面,此處沒有加異步等待處理,所以,用戶量大的朋友,請耐心等待(後期會加上)。

登錄成功以後,就會出現主窗體和系統托盤,主窗體包含最近聯繫人和通訊錄,系統托盤網上很多解決方案,可以自行查找。
登錄成功現在發現了一個問題,就是我有兩個微信號,其中一登錄以後是有數據的,另一個則沒有數據。


跟蹤代碼,發現返回的 json 是空的,也就是說沒有返回值,試驗了下周大神的代碼,發現也是空的,不清楚什麼情況,我同事的有的也是空的,這個一直沒有深究,等把功能基本都完善以後再看看問題所在。

二、主窗體模塊
1、主窗體的布局部分很簡單,採用了 grid 進行分隔,三列,上面的控制項如圖所示

大部分到沒什麼,可能大家比較疑惑的是我的聊天窗體為什麼是 listbox,這個東西的話,我認為,自己有自己的開發習慣,很多控制項都可以實現,panel 就可以。
RadioButton 的样式是用 path 画的,可以看我另一篇博客微信聊天和通讯录按钮样式

2、聊天列表里,未讀的消息上會有帶數字的小紅點,這個是用 button 寫的,item 的整體組成是 image(頭像)、button(未讀數)、textblock(暱稱、時間和聊天內容)
<Style x:Key="ListBoxItemChatStyle" TargetType="{x:Type ListBoxItem}">
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="ListBoxItem">
<Border>
<StackPanel x:Name="sp" Orientation="Horizontal" Height="{Binding Converter={StaticResource objectToHeight}}" Background="{Binding Converter={StaticResource objectToColor}}">
<Grid>
<Image Source="{Binding Icon}" Width="40" Height="40" Margin="10"/>
<Button Foreground="White" Visibility="{Binding UnReadCount,Converter={StaticResource countToVisibility}}" Content="{Binding UnReadCount}" HorizontalAlignment="Right" VerticalAlignment="Top" Margin="0,5" Style="{StaticResource CirButtonStyle}"/>
</Grid>
<Grid Width="176">
<Grid.RowDefinitions>
<RowDefinition/>
<RowDefinition/>
</Grid.RowDefinitions>
<TextBlock Grid.Row="0" Text="{Binding ShowName}" FontSize="15" HorizontalAlignment="Left" Margin="5,10,0,0"/>
<TextBlock Grid.Row="0" Text="{Binding LastTime}" FontSize="15" HorizontalAlignment="Right" Margin="0,10,5,0"/>
<TextBlock Grid.Row="1" Text="{Binding LastMsg}" FontSize="12" HorizontalAlignment="Left" Margin="5,0,0,0"/>
</Grid>
</StackPanel>
</Border>
<ControlTemplate.Triggers>
<Trigger Property="IsMouseOver" Value="true">
<Setter Property="Background" Value="#FFE2E4E6" TargetName="sp"/>
</Trigger>
<Trigger Property="IsSelected" Value="true">
<Setter Property="Background" Value="#FFCACDD3" TargetName="sp"/>
</Trigger>
</ControlTemplate.Triggers>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>

3、聊天內容部分用的是 scrollinglistbox,繼承自 listbox,但是重寫了裡面的 onitemschanged 屬性,保證可以時刻滾動到最後一行
public class ScrollingListBox : ListBox
{
protected override void OnItemsChanged(System.Collections.Specialized.NotifyCollectionChangedEventArgs e)
{
if (e.NewItems!=null)
{
int newItemCount = e.NewItems.Count;
if (newItemCount > 0)
this.ScrollIntoView(e.NewItems[newItemCount - 1]);
base.OnItemsChanged(e);
}
}
}
樣式部分是重寫控制項模板用的是 image(頭像),path(三角部分),textbox(內容部分)
<Style x:Key="ChatListBoxStyle" TargetType="{x:Type ListBox}">
<Setter Property="Background" Value="{DynamicResource {x:Static SystemColors.WindowBrushKey}}"/>
<Setter Property="BorderBrush" Value="{StaticResource ListBorder}"/>
<Setter Property="BorderThickness" Value="0"/>
<Setter Property="Foreground" Value="{DynamicResource {x:Static SystemColors.ControlTextBrushKey}}"/>
<Setter Property="ScrollViewer.HorizontalScrollBarVisibility" Value="Hidden"/>
<Setter Property="ScrollViewer.VerticalScrollBarVisibility" Value="Hidden"/>
<Setter Property="ScrollViewer.CanContentScroll" Value="true"/>
<Setter Property="ScrollViewer.PanningMode" Value="Both"/>
<Setter Property="Stylus.IsFlicksEnabled" Value="False"/>
<Setter Property="VerticalContentAlignment" Value="Center"/>
<Setter Property="ItemContainerStyle">
<Setter.Value>
<Style TargetType="ListBoxItem">
<Setter Property="Focusable" Value="False"/>
<Setter Property="Template">
<Setter.Value>
<ControlTemplate>
<StackPanel Orientation="Horizontal" Grid.Column="1" HorizontalAlignment="Stretch" VerticalAlignment="Top" FlowDirection="{Binding FlowDir}" Margin="15,5">
<Image Grid.Column="1" Source="{Binding Image}" Height="35" Width="35" VerticalAlignment="Top"/>
<Path Grid.Column="2" StrokeThickness="1" Stroke="{Binding TbColor}" Data="M12,13 L5,18 L12,23Z" Fill="{Binding TbColor}" Margin="0" SnapsToDevicePixels="True"/>
<TextBox Grid.Column="3" MaxWidth="355" TextWrapping="Wrap" FontSize="15" BorderBrush="{Binding TbColor}" Background="{Binding TbColor}" IsReadOnly="True" BorderThickness="0" Style="{StaticResource ChatTextBoxStyle}" FlowDirection="LeftToRight" Text="{Binding Message}"/>
</StackPanel>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
</Setter.Value>
</Setter>
</Style>
需要注意的是:此处必须要重写控件模板,而不能重写数据模板,虽然,很多情况下控件模板和数据模板可以得到的效果相同,但是此处,如果写数据模板的话,则自己发的信息不会在右侧,就算设置FlowDirection也没有用,大家可以自行尝试。
4、如果發送內容是空的情況下,則會有一個 tooltip 出現,此處的 tooltipye 也是重寫了樣式的 button,好定位,畢竟就算是最大化,位置也是不變的。

通訊錄部分,和聊天列表差不多,不過,由於需要進行分組,也就是 a、b……這種組合,所以用的 object 類型,在點選過程中,通過 is 來進行判別是不是 wechatuser,如果是,則進行轉換,來進一步處理。
大家可以看到上面那个好友是 同程旅游顾问<span …… 其实它是一个 emoji,只是现在我还没有做到那一部分,如果做到的话,则进行转换,如果谁有好的 emoji 处理方式希望告知,谢谢了。


當點選列表以後,並且轉換成功的情況下,則顯示出用戶的信息,通過內容是否未空,來判別是否要顯示
<Grid Grid.Row="1" Grid.RowSpan="2" HorizontalAlignment="Center" VerticalAlignment="Center" Visibility="{Binding ElementName=rb_friend,Path=IsChecked,Converter={StaticResource boolToVisibility}}" Margin="0,50,0,0">
<Grid.RowDefinitions>
<RowDefinition Height="Auto"/>
<RowDefinition Height="Auto"/>
<RowDefinition Height="Auto"/>
<RowDefinition Height="Auto"/>
<RowDefinition Height="Auto"/>
<RowDefinition Height="Auto"/>
<RowDefinition Height="Auto"/>
</Grid.RowDefinitions>
<Image Source="{Binding FriendInfo.Icon}" Grid.Row="0" Height="124" Width="124" HorizontalAlignment="Center"/>
<StackPanel Orientation="Horizontal" Grid.Row="1" HorizontalAlignment="Center">
<TextBlock Text="{Binding FriendInfo.NickName}" FontSize="30" Foreground="Black" FontWeight="Bold"/>
<Image Visibility="{Binding FriendInfo.Sex,Converter={StaticResource parameterToVisibility},ConverterParameter=2}" Source="/Image/female.png"/>
<Image Visibility="{Binding FriendInfo.Sex,Converter={StaticResource parameterToVisibility},ConverterParameter=1}" Source="/Image/male.png"/>
</StackPanel>
<TextBlock Text="{Binding FriendInfo.Signature}" Foreground="#FF919191" Grid.Row="2" HorizontalAlignment="Center"/>
<StackPanel Orientation="Horizontal" Visibility="{Binding FriendInfo.RemarkName,Converter={StaticResource epmtyToVisibility}}" Margin="10" Grid.Row="3" HorizontalAlignment="Center">
<TextBlock Text="备 注" Margin="0,0,10,0" FontSize="15"/>
<TextBlock Text="{Binding FriendInfo.RemarkName}" FontSize="15"/>
</StackPanel>
<StackPanel Orientation="Horizontal" Visibility="{Binding FriendInfo.Province,Converter={StaticResource epmtyToVisibility}}" Margin="10" Grid.Row="4" HorizontalAlignment="Center">
<TextBlock Text="地区" Margin="0,0,10,0" FontSize="15"/>
<TextBlock Text="{Binding FriendInfo.Province}" Margin="0,0,2,0" FontSize="15"/>
<TextBlock Text="{Binding FriendInfo.City}" FontSize="15"/>
</StackPanel>
<Button Content="发消息" Width="166" Height="37" Grid.Row="5" Command="{Binding FriendSendComamnd}" Margin="0,50,0,0" Style="{StaticResource FriSendButtonStyle}"/>
<Grid Grid.Row="0" Grid.RowSpan="7" Background="WhiteSmoke" Visibility="{Binding FriendInfo,Converter={StaticResource nullToVisibility}}"/>
</Grid>
點擊發消息按鈕,則跳轉回聊天頁面,然後,將當前的好友加入到聊天的第一項。
三、總結
做 wpf 微信 demo,用到了轉換器,轉換顏色,轉換顯隱;重寫了控制項的樣式,例如 button、radiobutton、listbox;然後 mvvm 模式下,bind 的用法,感覺這個 deom 對於初學者來說應該會有很大的幫助。
不過這個 demo 的 bug 和不完善的地方還有很多,例如系統托盤還沒有做閃爍,現在只能發送文字,最大化的問題。
系統托盤閃爍可以用 timer 和 opacity 來進行控制,比如來未讀消息了,則在進行時間間隔的控制顯隱。
後期會把 textbox 換成 richtextbox,這樣可以發送圖片和 emoji。
最大化問題,是我一直還沒有想到好的解決辦法,最大化的情況下會占據整個屏幕,而不把狀態欄空出來,網上的辦法都是重新設置 width 和 height,但是這樣的話,就要記錄原來的大小和位置,一直沒有找到可以重寫 windowstate.maximized 的方法,好像是不能重寫,所以比較糾結,希望哪位大神看完我的代碼以後,能夠給提供一下解決思路,謝謝了。