This article is republished as an original work with the authorization of the original author. Reprinting and sharing are welcome.
Original author: 眾尋
Original link: https://www.cnblogs.com/ZXdeveloper/p/6058206.html
Colleagues at the company have left, so the coming days might be busy, and I will have less time to improve the DEMO. Therefore, I will first record the simple DEMO I have built and continue to refine it later.
I have referenced the logs of two experts: Analysis of Part of the WEB Version WeChat Protocol, 【Fully Open Source】WeChat Client .NET Version
Especially the DEMO by Zhou Jianzhi, because many interactions with WeChat's server side borrowed heavily from his source code. It has been a huge help. You could say my work is a replica, but developed using WPF. The appearance is different, but the actual interaction is quite similar.
WeChat is divided into two parts: login and main body. Based on this, the WPF implementation also mainly uses these two windows.
I. Login Module
- The login part consists of two pages: QR code and user avatar retrieval (because it's based on WEB, there is no client-side login button, only QR code scanning is allowed for login).


After the program starts, it first obtains the QR code via a request, then starts a new thread to continuously poll the login status.
private void LoopLoginCheck()
{
object login_result = null;
// Loop to check the result of scanning the QR code on the phone
while (true)
{
login_result = ls.LoginCheck();
// Scanned but not logged in
if (login_result is ImageSource)
{
HeadImageSource = login_result as ImageSource;
// Broadcast, notify the LoginUC page to switch
Messenger.Default.Send<object>(null, "ShowLoginInfoUC");
}
// Login completed
if (login_result is string)
{
// Access the login redirect URL
ls.GetSidUid(login_result as string);
// Broadcast, hide the login page, open the main page
Messenger.Default.Send<object>(null, "HideLoginUC");
thread.Abort();
break;
}
//// Timeout
if (login_result is int)
{
//QRCodeImageSource = ls.GetQRCode();
// Return to the QR code page
Messenger.Default.Send<object>(null, "ShowQRCodeUC");
}
}
}
Because it's MVVM, broadcasting is used to switch pages, i.e., to fill the control in the middle of the login form with either the QR code or the avatar.
- You can see that the screenshots above include some background. This was automatically captured when using Snagit (a recommended screenshot tool). The window itself is that size; the extra part is transparent and used for the sliding effect of the QR code.

When the mouse passes over the QR code state, an animation occurs. In the avatar state, there is no animation; this is controlled by setting the Visibility property of the Image. You can check my other post WeChat QR Code Mouse Sliding Image Show/Hide Effect for the sliding effect.
- After the QR code is scanned and you click "Login" on the phone, it jumps to the main page. There is no asynchronous wait handling here, so if there are many users, please be patient (this will be added later).

After successful login, the main form and system tray appear. The main form includes recent contacts and the address book. There are many online solutions for the system tray; you can search for them.
A problem I found after successful login is that I have two WeChat accounts. One has data after login, while the other has no data.


Tracing the code, I found that the returned Json is empty, meaning there is no return value. I also tested Zhou's code and found it was also empty. I don't know what the situation is. Some of my colleagues also have empty data. I haven't delved into this yet; I'll check it after I have more features completed.

II. Main Form Module
- The layout of the main form is simple. It uses a Grid with three columns. The controls are shown in the figure.

Most of it is straightforward. You might be wondering why my chat window uses a ListBox. I believe everyone has their own development habits; many controls can achieve this, such as Panel.
The RadioButton style is drawn using Path. You can check my other post WeChat Chat and Address Book Button Styles

- In the chat list, unread messages have a small red dot with a number. This is implemented with a Button. The overall composition of each Item is: Image (avatar), Button (unread count), TextBlock (nickname, time, and chat content)
<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>

- The chat content area uses ScrollingListBox, which inherits from ListBox but overrides the OnItemsChanged property to ensure it always scrolls to the last line.
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);
}
}
}
The style part overrides the control template using Image (avatar), Path (triangle part), and TextBox (content part).
<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>
It is important to note: Here you must override the control template, not the data template. Although in many cases the control template and data template can achieve the same effect, if you write a data template here, messages sent by yourself will not appear on the right side, even if you set FlowDirection. You can try it yourself.
- If the sent content is empty, a ToolTip will appear. The ToolTip here also uses a Button with an overridden style for better positioning; even when maximized, the position remains unchanged.

The address book part is similar to the chat list, but since it requires grouping, i.e., the A, B... categories, it uses the Object type. When selecting, the is operator is used to determine whether it is a WeChatUser; if so, it is cast for further processing.
You can see that the friend above is 同程旅游顾问<span …… It is actually an emoji, but I haven't implemented that part yet. When I do, it will be converted. If anyone has a good way to handle emoji, please let me know. Thank you.


When an item is selected and the conversion is successful, the user's information is displayed, and whether to show it is determined by whether the content is empty.
<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>
Clicking the "Send Message" button jumps back to the chat page and adds the current friend to the first item in the chat list.
III. Summary
In creating this WPF WeChat DEMO, I used converters for color and visibility conversion; overridden control templates for Button, RadioButton, ListBox; and the MVVM pattern with Bind usage. I believe this DEMO will be very helpful for beginners.
However, this DEMO still has many bugs and unfinished parts. For example, the system tray doesn't flash yet, only text can be sent, and there is the maximization issue.
System tray flashing can be controlled with Timer and Opacity, e.g., when an unread message arrives, toggle visibility at intervals.
Later, I will replace the TextBox with a RichTextBox to support sending images and emoji.
As for the maximization issue, I haven't found a good solution yet. When maximized, the window occupies the entire screen without leaving the taskbar empty. Online solutions all involve resetting Width and Height, but that requires recording the original size and position. I have never found a way to override WindowState.Maximized, and it seems impossible to override. So I'm quite conflicted. I hope someone who reads my code can provide a solution. Thank you.
Source code here: https://github.com/yanchao891012/WPF_WeChat/