本記事はユーザー投稿によるものです
著者: 陈显达
原文タイトル: 【微信自動化】c#を使用して微信自動化を実現
原文リンク: https://www.cnblogs.com/1996-Chinese-Chen/p/17663064.html
はじめに
先月、あるグループでだらだらと時間を潰していると、ある方が微信自動化用のクラスライブラリを共有していました。そこで彼のDemoをダウンロードし、その本質はマウスをシミュレートしてUIを操作し、UI自動化を実現するものでした。その後、自分なりに試行錯誤して研究し、シンプルなサンプルを作成しました。友達リストの取得、チャットリストの取得、最後に受信または送信したメッセージの日時、最後のチャット内容、さらに自動でモーメントをスクロールし、誰が何のテキストを投稿したか、添付画像や動画の種類、投稿日時を取得します。また、取得した友達リストを基に指定した友達にメッセージを送信する機能も実装しました。
まずはスクリーンショットをいくつか掲載して興味を引きます。
友達リストの取得とメッセージ送信

チャット履歴の取得

モーメントのスクロール

本文
前置きはこれくらいにして、早速始めましょう。まず目に入るのはインターフェースです。左側は友達リストの取得、右側は選択した友達リストに基づいてメッセージを送信するためのRichTextBox、中央はチャットリスト、友達名、最後のチャット内容とその日時、最も右側はモーメントの内容を取得し、モーメントをスクロールして友達が投稿した内容や添付メディアの種類(画像か動画か)、投稿日時を表示します。
まず、NuGetで2つのパッケージ(FlaUI.Core と FlaUI.UIA3)をダウンロードする必要があります。これらを使用してマウスシミュレーションとUI自動化を実現します。それではコードを見ていきましょう。

上記はインターフェース全体のスクリーンショットです。次にコードについて説明します。フォームが作成された時点で、微信のプロセスIDを取得し、友達リスト、チャットリスト、モーメント用のCancellationTokenSourceとそれに関連するCancellationTokenに値を代入します。これにより中断・キャンセル機能を実現します。また、上部のlistはモーメント情報を格納するために使用し、下部のContentはチャットリストの内容を格納します。Keyはチャット相手のニックネーム、Valueは最後のチャット内容です。その下のSendInputはマウススクロールをシミュレートするために使用し、チャットリストやモーメントの内容、友達リストをスクロールして取得できるようにします。さらに、FindWindowとGetWindowThreadProcessIDは、ウィンドウ名から対応するプロセスIDを検索するために使用します。これは、モーメントのポップアップウィンドウをダブルクリックで開いた場合、Processを使って取得するのが不便なため、直接これらの関数を呼び出してモーメントのポップアップウィンドウを検索します。
private List<dynamic> list = new List<dynamic>();
private Dictionary<string, string> Content = new Dictionary<string, string>();
/// <summary>
/// スクロールバーシミュレーション
/// </summary>
/// <param name="nInputs"></param>
/// <param name="pInputs"></param>
/// <param name="cbSize"></param>
/// <returns></returns>
[DllImport("user32.dll", SetLastError = true, CharSet = CharSet.Auto)]
public static extern uint SendInput(uint nInputs, INPUT[] pInputs, int cbSize);
//ウィンドウハンドルを名前で取得
[DllImport("user32.dll", EntryPoint = "FindWindow")]
private extern static IntPtr FindWindow(string lpClassName, string lpWindowName);
//ハンドルからプロセスIDを取得
[DllImport("User32.dll", CharSet = CharSet.Auto)]
public static extern int GetWindowThreadProcessId(IntPtr hwnd, out int ID);
public Form1()
{
InitializeComponent();
GetWxHandle();
GetFriendTokenSource = new CancellationTokenSource();
GetFriendCancellationToken = GetFriendTokenSource.Token;
ChatListTokenSource = new CancellationTokenSource();
ChatListCancellationToken = ChatListTokenSource.Token;
FriendTokenSource = new CancellationTokenSource();
FriendCancellationToken = FriendTokenSource.Token;
}
private CancellationToken FriendCancellationToken { get; set; }
private CancellationTokenSource FriendTokenSource { get; set; }
private CancellationToken ChatListCancellationToken { get; set; }
private CancellationTokenSource ChatListTokenSource { get; set; }
private CancellationToken GetFriendCancellationToken { get; set; }
private CancellationTokenSource GetFriendTokenSource { get; set; }
private int ProcessId { get; set; }
private Window wxWindow { get; set; }
private bool IsInit { get; set; } = false;
void GetWxHandle()
{
var process = Process.GetProcessesByName("Wechat").FirstOrDefault();
if (process != null)
{
ProcessId = process.Id;
}
}
次に、取得したプロセスIDとFlaUIをバインドし、微信のメインUIウィンドウを取得します。
void InitWechat()
{
IsInit = true;
//微信プロセスIDでFLAUIにバインド
var application = FlaUI.Core.Application.Attach(ProcessId);
var automation = new UIA3Automation();
//微信window自動化操作オブジェクトを取得
wxWindow = application.GetMainWindow(automation);
//微信を前面に表示
}
次は友達リストの取得です。微信インターフェースがロードされているか確認し、ロードされていなければInitWeChatメソッドを呼び出します。その後、メインウィンドウがnullでないことを確認し、アクティブなウィンドウに設定します。そして、メインウィンドウでUIコントロールの名前が「連絡先」であるものを探してクリックをシミュレートし、チャット画面から連絡先画面に切り替えます。デフォルトでは最初に「新しい友達」が表示されますが、現在のリストがどこにあっても、友達リストの取得ボタンをクリックすれば、たとえ最上部の「新しい友達」にいなくても、マウススクロールをシミュレートして友達リストを取得できます。次にFindAllDescendantsを呼び出して、メインウィンドウのすべての子ノードを取得し、その中で親ノードがnullではなく、親ノードのNameが「連絡先」であるノードを探します。親のNameが「連絡先」である理由は、友達リストがすべて「連絡先」という親ノードの下に属しているからです。見つけたら、それらの連絡先を走査し、名前が空でないものをフィルタリングします(同名の場合はフィルタリングされる可能性がありますが、特に処理はしていません)。また、見つかったコントロールタイプがListItemであることを確認します。連絡先自体がリストであり、そのサブアイテムはリスト項目となるため、このフィルタリングが必要です。これで友達リストを取得し、インターフェースに追加します。最後にScrollメソッドを呼び出して700ピクセル分スクロールをシミュレートします。この値はPCの画面サイズや微信の最大化状態によって異なる可能性があるため、状況に応じて設定してください。最後に友達リスト取得をキャンセルするイベントを記述しています。
/// <summary>
/// 友達リストを取得
/// </summary>
/// <param name="sender"></param>
/// <param name="e"></param>
private void button1_Click(object sender, EventArgs e)
{
if (!IsInit)
{
InitWechat();
}
if (wxWindow != null)
{
if (wxWindow.AsWindow().Patterns.Window.PatternOrDefault != null)
{
//微信ウィンドウをデフォルトフォーカス状態に設定
wxWindow.AsWindow().Patterns.Window.Pattern.SetWindowVisualState(FlaUI.Core.Definitions.WindowVisualState.Normal);
}
}
wxWindow.FindAllDescendants().Where(s => s.Name == "通讯录").FirstOrDefault().Click(false);
wxWindow.FindAllDescendants().Where(s => s.Name == "新的朋友").FirstOrDefault()?.Click(false);
string LastName = string.Empty;
var list = new List<AutomationElement>();
var sync = SynchronizationContext.Current;
Task.Run(() =>
{
while (true)
{
if (GetFriendCancellationToken.IsCancellationRequested)
{
break;
}
var all = wxWindow.FindAllDescendants();
var allItem = all.Where(s => s.Parent != null && s.Parent.Name == "联系人").ToList();
var sss = all.Where(s => s.ControlType == ControlType.Text && !string.IsNullOrWhiteSpace(s.Name)).ToList();
foreach (var item in allItem)
{
if (item.Name != null && item.ControlType == ControlType.ListItem && !string.IsNullOrWhiteSpace(item.Name) && !listBox1.Items.Contains(item.Name.ToString()))
{
sync.Post(s =>
{
listBox1.Items.Add(s);
}, item.Name.ToString());
}
}
Scroll(-700);
}
}, GetFriendCancellationToken);
}
private void button4_Click(object sender, EventArgs e)
{
GetFriendTokenSource.Cancel();
}

次はモーメントを取得するイベントです。プロセスIDの取得は前述のProcessを使った方法と同じですが、ここではFindWindowを使わなくても問題ないかもしれません。ウィンドウを見つけた後、Windowの操作オブジェクトを取得し、モーメントをクリックして表示されるモーメント画面を取得します。そして最初の項目をクリックしてマウスを移動させ、後で自動スクロールができるようにします。ループ内で、この画面のすべての子要素を取得し、親クラスが「モーメント」、リストであるListItemを探します。見つけたら、そのコレクションを走査します。モーメントのニックネーム、メディアタイプ、日時、モーメントのテキスト内容はすべてNameプロパティに含まれているため、そのフォーマットに従って分割し、対応する日時、ニックネーム、モーメント内容、メディアタイプなどを取得し、最後にDataGridViewに追加します。
private void button3_Click(object sender, EventArgs e)
{
if (!IsInit)
{
InitWechat();
}
if (wxWindow != null)
{
if (wxWindow.AsWindow().Patterns.Window.PatternOrDefault != null)
{
//微信ウィンドウをデフォルトフォーカス状態に設定
wxWindow.AsWindow().Patterns.Window.Pattern.SetWindowVisualState(FlaUI.Core.Definitions.WindowVisualState.Normal);
}
}
var a = Process.GetProcesses().Where(s => s.ProcessName == "朋友圈");
wxWindow.FindAllDescendants().Where(s => s.Name == "朋友圈").FirstOrDefault().Click(false);
var handls = FindWindow(null, "朋友圈");
if (handls != IntPtr.Zero)
{
GetWindowThreadProcessId(handls, out int FridId);
var applicationFrid = FlaUI.Core.Application.Attach(FridId);
var automationFrid = new UIA3Automation();
//微信window自動化操作オブジェクトを取得
var Friend = applicationFrid.GetMainWindow(automationFrid);
Friend.FindAllDescendants().FirstOrDefault(s => s.ControlType == ControlType.List).Click(false);
var sync = SynchronizationContext.Current;
Task.Run(async () =>
{
while (true)
{
try
{
if (FriendCancellationToken.IsCancellationRequested)
{
break;
}
var allInfo = Friend.FindAllDescendants();
var itema = allInfo.Where(s => s.ControlType == ControlType.ListItem && s.Parent.Name == "朋友圈" && s.Parent.ControlType == ControlType.List);
if (itema != null)
{
foreach (var item in itema)
{
var ass = item.FindAllDescendants().FirstOrDefault(s => s.ControlType == ControlType.Text);
//ass.FocusNative();
//ass.Focus();
var index = item.Name.IndexOf(':');
var name = item.Name.Substring(0, index);
var content = item.Name.Substring(index + 1);
var split = content.Split("\n");
if (split.Length > 3)
{
var time = split[split.Length - 2];
var mediaType = split[split.Length - 3];
var FriendContent = split[0..(split.Length - 3)];
var con = string.Join(",", FriendContent);
if (list.Any(s => s.Content == con))
{
continue;
}
sync.Post(s =>
{
dataGridView2.Rows.Add(name, s, mediaType, time);
dynamic entity = new
{
Name = name,
Content = s,
MediaType = mediaType,
Time = time
};
list.Add(entity);
}, con);
}
}
Scroll(-500);
await Task.Delay(100);
}
}
catch (Exception ex)
{
continue;
}
}
});
}
}
private void button6_Click(object sender, EventArgs e)
{
FriendTokenSource.Cancel();
}
続いてチャットリストの取得と指定した友達へのメッセージ送信機能です。以下のコードの最初の部分では、アクティブウィンドウに設定する処理を行っています。すべての子要素を取得し、セッションに属する子ノード(ListItem)を探します。折りたたまれたグループチャットはフィルタリングします。折りたたまれたグループチャットをクリックした場合は、シミュレートで戻る操作が必要ですが、ここでは具体的なコードは書いていません。ただし、簡単に実現できます。該当するチャットリストを見つけたら、各チャット相手を走査し、XPathを使用して条件に合うテキストを取得します。このテキストには日時、内容、ニックネームが含まれています。XPathについて、構造がわからない場合は、C:\Program Files (x86)\Windows Kits\10\bin\10.0.19041.0\x64 にある inspect.exe ツールを使用してUI構造を確認し、それに基づいてXPathを記述してください。これらの情報を取得したら、インターフェースに追加し、マウススクロールをシミュレートしてチャットリストを取得します。
private void button2_Click(object sender, EventArgs e)
{
if (!IsInit)
{
InitWechat();
}
if (wxWindow != null)
{
if (wxWindow.AsWindow().Patterns.Window.PatternOrDefault != null)
{
//微信ウィンドウをデフォルトフォーカス状態に設定
wxWindow.AsWindow().Patterns.Window.Pattern.SetWindowVisualState(FlaUI.Core.Definitions.WindowVisualState.Normal);
}
}
wxWindow.FindAllDescendants().Where(s => s.Name == "聊天").FirstOrDefault().Click(false);
wxWindow.FindAllDescendants().Where(s => s.Name == "妈妈").FirstOrDefault().Click(false);
var sync = SynchronizationContext.Current;
Task.Run(async () =>
{
object obj;
while (true)
{
var all = wxWindow.FindAllDescendants();
try
{
if (ChatListCancellationToken.IsCancellationRequested)
{
break;
}
var allItem = all.Where(s => s.ControlType == ControlType.ListItem && !string.IsNullOrEmpty(s.Name) && s.Parent.Name == "会话" && s.Name != "折叠的群聊");
foreach (var item in allItem)
{
var allText = item.FindAllByXPath("//*/Text");
if (allText != null && allText.Length >= 3)
{
var name = allText[0].Name;
var time = allText[1].Name;
var content = allText[2].Name;
if (Content.ContainsKey(name))
{
var val = Content[name];
if (val != content)
{
Content.Remove(name);
Content.Add(name, content);
}
}
else
{
Content.Add(name, content);
}
sync.Post(s =>
{
dataGridView1.Rows.Add(item.Name, content, time);
}, null);
}
}
Scroll(-700);
await Task.Delay(100);
}
catch (Exception)
{
continue;
}
}
}, ChatListCancellationToken);
}
private void button5_Click(object sender, EventArgs e)
{
ChatListTokenSource.Cancel();
}
次に送信ボタンのイベントです。主な機能は、選択した友達リストに基づいて、RichTextBoxに入力されたメッセージを送信することです。主要なコードブロックでは、PC版微信の検索ボックスを取得し、フォーカスを設定してクリックをシミュレートし、選択した友達の名前を検索ボックスに入力します。500ミリ秒待機した後、再度画面の子要素を取得し、検索結果が表示されるようにします。待機しないと取得できません。見つかったらデフォルトの最初の項目をクリックし、チャット画面に移動します。チャット画面を取得したら、入力用のテキストボックス(コード内ではMsgBox)を取得し、そのTextプロパティにRichTextBoxの値を設定します。その後、送信ボタンを見つけてクリックをシミュレートし、自動送信を実現します。
private async void button7_Click(object sender, EventArgs e)
{
var sendMsg=richTextBox1.Text.Trim();
var itemName = listBox1.SelectedItem?.ToString();
if (!IsInit)
{
InitWechat();
}
if (wxWindow != null)
{
if (wxWindow.AsWindow().Patterns.Window.PatternOrDefault != null)
{
//微信ウィンドウをデフォルトフォーカス状態に設定
wxWindow.AsWindow().Patterns.Window.Pattern.SetWindowVisualState(FlaUI.Core.Definitions.WindowVisualState.Normal);
}
}
var search=wxWindow.FindAllDescendants().FirstOrDefault(s => s.Name == "搜索");
search.FocusNative();
search.Focus();
search.Click();
await Task.Delay(500);
var text=wxWindow.FindAllDescendants().FirstOrDefault(s => s.Name == "搜索").Parent;
if (text!=null)
{
await Task.Delay(500);
var txt=text.FindAllChildren().FirstOrDefault(s=>s.ControlType==ControlType.Text) .AsTextBox();
txt.Text = itemName;
await Task.Delay(500);
var item = wxWindow.FindAllDescendants().Where(s => s.Name==itemName&&s.ControlType==ControlType.ListItem).ToList();
wxWindow.FocusNative();
if (item!=null&& item.Count>0&&!string.IsNullOrWhiteSpace(sendMsg))
{
if (item.Count<=1)
{
item.FirstOrDefault().Click();
}
else
{
item.FirstOrDefault(s => s.Parent != null && s.Parent.Name.Contains("@str:IDS_FAV_SEARCH_RESULT")).Click();
}
var msgBox = wxWindow.FindFirstDescendant(x => x.ByControlType(FlaUI.Core.Definitions.ControlType.Text)).AsTextBox();
msgBox.Text = sendMsg;
var button = wxWindow.FindAllDescendants().Where(s => s.Name == "发送(S)").FirstOrDefault();
button?.Click();
}
}
}
下記は私が取得した友達リスト、モーメントリスト、チャットリストの情報です。

以下はC#でWin APIを呼び出してマウススクロールをシミュレートするコードです。SendInputの詳細については、公式ドキュメント SendInput function (winuser.h) を参照してください。
#region Scroll Event
void Scroll(int scroll)
{
INPUT[] inputs = new INPUT[1];
// マウススクロールイベントを設定
inputs[0].type = InputType.INPUT_MOUSE;
inputs[0].mi.dwFlags = MouseEventFlags.MOUSEEVENTF_WHEEL;
inputs[0].mi.mouseData = (uint)scroll;
// 入力イベントを送信
SendInput(1, inputs, Marshal.SizeOf(typeof(INPUT)));
}
public struct INPUT
{
public InputType type;
public MouseInput mi;
}
// 入力タイプ
public enum InputType : uint
{
INPUT_MOUSE = 0x0000,
INPUT_KEYBOARD = 0x0001,
INPUT_HARDWARE = 0x0002
}
// マウス入力構造体
public struct MouseInput
{
public int dx;
public int dy;
public uint mouseData;
public MouseEventFlags dwFlags;
public uint time;
public IntPtr dwExtraInfo;
}
// マウスイベントフラグ
[Flags]
public enum MouseEventFlags : uint
{
MOUSEEVENTF_MOVE = 0x0001,
MOUSEEVENTF_LEFTDOWN = 0x0002,
MOUSEEVENTF_LEFTUP = 0x0004,
MOUSEEVENTF_RIGHTDOWN = 0x0008,
MOUSEEVENTF_RIGHTUP = 0x0010,
MOUSEEVENTF_MIDDLEDOWN = 0x0020,
MOUSEEVENTF_MIDDLEUP = 0x0040,
MOUSEEVENTF_XDOWN = 0x0080,
MOUSEEVENTF_XUP = 0x0100,
MOUSEEVENTF_WHEEL = 0x0800,
MOUSEEVENTF_HWHEEL = 0x1000,
MOUSEEVENTF_MOVE_NOCOALESCE = 0x2000,
MOUSEEVENTF_VIRTUALDESK = 0x4000,
MOUSEEVENTF_ABSOLUTE = 0x8000
}
const int MOUSEEVENTF_WHEEL = 0x800;
#endregion
おわりに
このクラスライブラリを使用すれば、自動応答ボットやモーメントの特定の友達の更新を購読する機能、メッセージ購読機能、公式アカウントなどの情報収集も実現できます。
以上がFlaUIを使用した微信自動化のシンプルなDemoです。確かQQもシミュレートできたはずで、以前簡単に試したところ、いくつかの情報を取得できました。コードアドレス: https://gitee.com/cxd199645/we-chat-auto.git。ぜひ議論にご参加ください。