本記事は原作者の許可を得てオリジナルとして二次共有されており、転載・共有は自由です。
原文著者:眾尋
原文リンク:https://www.cnblogs.com/ZXdeveloper/p/6058206.html
会社の同僚が退職したため、これから忙しくなり、DEMOを完成させる時間が減るかもしれません。そこで、簡易DEMOの全体像を先に記録し、後日徐々に改良していくことにしました。
参考にした大先輩の記事:WEB版微信プロトコルの部分機能分析、[完全オープンソース]微信クライアント.NET版
特に周見智大先輩のDEMOは、微信サーバーとのやり取りの多くを参考にさせていただき、大いに助かりました。実質的にはそのコピー版を作ったようなもので、WPFで開発した点が異なり、外観は違いますが、実際のやり取りはほぼ同じです。
微信は二つの部分に分かれています。ログインとメイン部分です。これに基づき、WPFも主にこの二つのフォームで実装しています。
一、ログインモジュール
- ログイン部分はQRコードとユーザーアバター取得の2ページに分かれています(WEBベースのため、クライアント側のログインボタンはなく、QRコードスキャンでのみログインできます)。


プログラム起動後、まずリクエストを送信してQRコードを取得し、その後新しいスレッドを起動してログイン状態を継続的にポーリングチェックします。
private void LoopLoginCheck()
{
object login_result = null;
//循環してスマホによるQRコードスキャン結果を判定
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();
//QRコードページに戻る
Messenger.Default.Send<object>(null, "ShowQRCodeUC");
}
}
}
MVVMのため、ブロードキャストを使用してページの切り替え操作を行います。ログインフォームの中央に表示するコントロールがQRコードかアバターかを制御します。
- 上記のスクリーンショットには背景の一部が含まれていますが、これはSnagit(お勧めのスクリーンショットツールです)でスクリーンショットを撮った際に自動で切り取られたものです。フォーム自体のサイズはそのままで、余分な部分は透明になっており、QRコードがスライドして現れるアニメーション効果を実現しています。

QRコード表示時にマウスを乗せるとアニメーションが再生されますが、アバター表示時にはアニメーションはありません。これはImageのVisibilityプロパティを設定して制御しています。スライド効果については、私の別のブログ記事「微信 QRコード マウススライド 画像表示・非表示効果」を参照してください。
- QRコードスキャンが成功し、スマホ側でログインボタンを押すとメインページに遷移します。ここでは非同期待機処理を入れていないため、利用者数が多い場合はしばらくお待ちください(後日追加予定)。

ログイン成功後、メインフォームとシステムトレイが表示されます。メインフォームには最近の連絡先とアドレス帳が含まれています。システムトレイについてはネット上に多くの解決策がありますので、各自で検索してください。
ログイン成功時に問題が一つ見つかりました。私が二つの微信アカウントを持っていて、一つはログイン後にデータがありますが、もう一つはデータがありません。


コードを追跡したところ、返ってくるJsonが空、つまり戻り値がありませんでした。周大先輩のコードでも試しましたが、同じく空でした。原因は不明です。同僚のアカウントでも空のものがありました。この問題は深く追及しておらず、機能が一通り完成した後に原因を調べる予定です。

二、メインフォームモジュール
- メインフォームのレイアウトはシンプルで、Gridを使用して3列に分割しています。上部のコントロールは図の通りです。

大半は特に問題ありませんが、チャットフォームにListBoxを使用している点が気になるかもしれません。これは開発者の習慣に依るもので、多くのコントロールで実現可能で、panelでも構いません。
RadioButtonのスタイルはpathで描画しています。別のブログ記事「微信 チャットとアドレス帳ボタンのスタイル」を参照してください。

- チャットリストでは、未読メッセージに数字のついた小さな赤い点が表示されます。これはButtonで実装しています。アイテムの構成は、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>

- チャット内容の部分は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を設定しても効果がありません。ご自身で試してみてください。
- 送信内容が空の場合、ToolTipが表示されます。このToolTipもスタイルを再定義したButtonを使用しており、位置が固定されています。最大化しても位置は変わりません。

アドレス帳部分はチャットリストとほぼ同じですが、グループ分け(A、B...のような)が必要なため、Object型を使用しています。選択時にis演算子でWeChatUserかどうかを判定し、該当する場合は変換して処理を行います。
上図で友達が「同程旅游顾问<span ……」と表示されていますが、これは絵文字(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の使い方も含まれており、このDEMOは初心者にとって非常に役立つものだと思います。
ただし、このDEMOにはバグや未完成な部分が多くあります。例えば、システムトレイの点滅機能が未実装で、現時点ではテキストのみ送信可能です。最大化の問題もあります。
システムトレイの点滅はTimerとOpacityを使って制御できます。例えば、未読メッセージが来た場合に時間間隔で表示・非表示を切り替えることができます。
今後はTextBoxをRichTextBoxに変更し、画像や絵文字の送信を可能にする予定です。
最大化の問題については、まだ良い解決策を思いついていません。最大化時には画面全体を占有し、タスクバーを空けることができません。ネット上の解決策はWidthとHeightを再設定する方法ですが、その場合元のサイズと位置を記録する必要があります。WindowState.Maximizedを再定義する方法が見つからず、オーバーライドできないようです。ですので、大変悩んでいます。どなたか私のコードをご覧になった上で、解決のヒントをいただけないでしょうか。よろしくお願いします。
ソースコードはこちら:https://github.com/yanchao891012/WPF_WeChat/