はじめに
業務アプリケーションの開発において、ユーザーがログアウトする際にMicrosoft Edgeで開いている特定のWebページだけを自動で閉じる必要が出てきました。Edge全体のプロセスを終了させる方法も考えましたが、閉じたくないWebページもあり、特定のタブのみを対象にする必要がありました。
本記事では、C#とUIAutomationを利用して、指定したタブだけを閉じる方法を記します。ここで紹介するコードは、2024年8月時点で動作確認したものとなります。
目次
やりたいこと
この状態から、タブのタイトルである「Google」という文字列を指定しタブを閉じて、
環境
- OS:Windows 11
- ブラウザ:Microsoft Edge 107.0.1418.52、127.0.2651.98
- 開発言語:C#(.NET8)
- IDE:Visual Studio 2022
UIAutomationとは
まず、Microsoft Learnによる説明は以下の通りです。
UI オートメーションは、デスクトップ上のほとんどのユーザー インターフェイス (UI) 要素へのプログラムによるアクセスを提供し、スクリーン リーダーなどの補助技術製品が UI に関する情報をエンド ユーザーに提供したり、標準入力方式以外の方法で UI を操作したりできるようにします。 また、UI オートメーションにより、自動テスト スクリプトが UI と対話できるようになります。
この技術は、デスクトップアプリケーション内のツリー構造にあるメニューやボタンなどの子要素に、外部プロセスからアクセスして操作するために使用される、ということのようです。
なお、UIAutomationは.NET Framework 4.8.1まではフレームワークに含まれていましたが、.NET Core以降のフレームワークでは公式にはサポートされていません。これは、UIAutomationがWindows専用の技術であるのに対し、.NET Core以降ではクロスプラットフォーム対応を重視しているためです。しかし、.NET 8でも使えることを確認しましたので、次節でその手順を説明します。
サンプルコード
準備
C#でUIAutomationを利用するためには、次の2つのアセンブリを参照する必要があります。
- UIAutomationClient.dll
- UIAutomationTypes.dll
.NET Frameworkでは、参照マネージャーのアセンブリの検索でこれらを見つけることができますが、.NET 8では出てきません。しかし、これらのDLLはWindows SDKに含まれるため、以下のようにDLLを直接参照することで利用できるようになります。
パスはSDKのバージョンやインストール場所によって異なる可能性がありますが、私の環境では下記を参照に追加しました。
- C:\Program Files (x86)\Reference Assemblies\Microsoft\Framework.NETFramework\v4.8.1\UIAutomationClient.dll
- C:\Program Files (x86)\Reference Assemblies\Microsoft\Framework.NETFramework\v4.8.1\UIAutomationTypes.dll
これで、System.Windows.Automation
名前空間をusingできるようになりました。
呼び出し側のコード
Edgeのタブを閉じる処理を1つのstaticクラスにまとめました。先にそのクラスを呼び出す側のコードを示します。引数に閉じたいタブのタイトルを指定します。複数のタブが同じタイトルを持っていれば、それらをすべて閉じます。Edgeのウィンドウが複数起動している場合も、すべてのウィンドウのタブが対象となります。
EdgeAutomation.CloseTab("タブのタイトル");
Edgeのタブを閉じるクラス
呼び出される側のEdgeのタブを閉じる処理を持つクラスのコードを示します。
using System.Diagnostics;
using System.Windows.Automation;
namespace CloseEdgeTabSampleNET8;
public static class EdgeAutomation
{
public static void CloseTab(string targetTabTitle)
{
if (string.IsNullOrEmpty(targetTabTitle))
{
return;
}
Process? edgeProcess = FindEdgeMainProcess();
if (edgeProcess == null)
{
return;
}
AutomationElementCollection edgeWindows = FindEdgeWindows(edgeProcess.Id);
if (edgeWindows.Count == 0)
{
return;
}
foreach (AutomationElement edgeWindow in edgeWindows)
{
AutomationElementCollection tabItems = FindEdgeTabs(edgeWindow);
foreach (AutomationElement tabItem in tabItems)
{
try
{
if (tabItem.Current.Name.Contains(targetTabTitle))
{
CloseTab(tabItem);
}
}
catch (ElementNotAvailableException ex)
{
Console.WriteLine(ex.Message);
}
}
}
}
private static Process? FindEdgeMainProcess()
{
foreach (Process proc in Process.GetProcessesByName("msedge"))
{
if (proc.MainWindowHandle != IntPtr.Zero)
{
return proc;
}
}
return null;
}
private static AutomationElementCollection FindEdgeWindows(int processId)
{
return AutomationElement.RootElement.FindAll(
TreeScope.Children,
new AndCondition(
new PropertyCondition(
AutomationElement.ProcessIdProperty,
processId),
new PropertyCondition(
AutomationElement.ControlTypeProperty,
ControlType.Window)));
}
private static AutomationElementCollection FindEdgeTabs(AutomationElement edgeWindow)
{
return edgeWindow.FindAll(
TreeScope.Descendants,
new PropertyCondition(
AutomationElement.ControlTypeProperty,
ControlType.TabItem));
}
private static AutomationElement? FindCloseButton(AutomationElement tabItem)
{
// バージョン 127.0.2651.98→EdgeTabCloseButton
// バージョン 107.0.1418.52→TabCloseButton
return tabItem.FindFirst(
TreeScope.Descendants,
new OrCondition(
new AndCondition(
new PropertyCondition(
AutomationElement.ControlTypeProperty,
ControlType.Button),
new PropertyCondition(
AutomationElement.ClassNameProperty,
"EdgeTabCloseButton")),
new AndCondition(
new PropertyCondition(
AutomationElement.ControlTypeProperty,
ControlType.Button),
new PropertyCondition(
AutomationElement.ClassNameProperty,
"TabCloseButton"))));
}
private static void CloseTab(AutomationElement tabItem)
{
AutomationElement? closeButton = FindCloseButton(tabItem);
if (closeButton == null)
{
return;
}
InvokePattern? invokePattern =
closeButton.GetCurrentPattern(InvokePattern.Pattern) as InvokePattern;
if (invokePattern == null)
{
return;
}
invokePattern.Invoke();
}
}
サンプルコードの説明
UIAutomationを使ってEdgeを操作するには、まずEdgeのメインプロセスを取得します。Edgeでタブを開くとEdgeのプロセスが複数起動しますが、メインプロセスを経由してツリー構造配下の子要素にアクセスすることになるようです。
private static Process? FindEdgeMainProcess()
{
foreach (Process proc in Process.GetProcessesByName("msedge"))
{
if (proc.MainWindowHandle != IntPtr.Zero)
{
return proc;
}
}
return null;
}
次にEdgeのウィンドウを取得します。ルート配下の要素を取得するために、AutomationElement.RootElement
というstaticオブジェクトが用意されています。FindAll
メソッドの第一引数は、検索の範囲です。ウィンドウはプロセスの直下にあるため、直接の子のみを対象とするChildren
を指定しています。第二引数は、プロセスIDと要素の種別としてWindow
を指定しています。Edgeのウィンドウが複数存在する場合、戻り値が複数となります。
private static AutomationElementCollection FindEdgeWindows(int processId)
{
return AutomationElement.RootElement.FindAll(
TreeScope.Children,
new AndCondition(
new PropertyCondition(
AutomationElement.ProcessIdProperty,
processId),
new PropertyCondition(
AutomationElement.ControlTypeProperty,
ControlType.Window)));
}
次はウィンドウ配下のタブを取得します。タブはEdgeのツリー構造のどこに位置しているか定かでないため、FindAll
メソッドの第一引数に子孫を含むDescendants
を指定しています。第二引数にはコントロールの種別としてTabItem
を指定しています。
private static AutomationElementCollection FindEdgeTabs(AutomationElement edgeWindow)
{
return edgeWindow.FindAll(
TreeScope.Descendants,
new PropertyCondition(
AutomationElement.ControlTypeProperty,
ControlType.TabItem));
}
タブを閉じるためには、タブが持つ「閉じるボタン」を探し出し、それを擬似的にクリックすることで実現します。「閉じるボタン」を見つけるにはクラス名を指定するのですが、Edgeのバージョンによって差異があるようです。私が検証できたのは2つのバージョンのみのため、他のバージョンでは動かないことがあるかもしれません。
private static AutomationElement? FindCloseButton(AutomationElement tabItem)
{
// バージョン 127.0.2651.98→EdgeTabCloseButton
// バージョン 107.0.1418.52→TabCloseButton
return tabItem.FindFirst(
TreeScope.Descendants,
new OrCondition(
new AndCondition(
new PropertyCondition(
AutomationElement.ControlTypeProperty,
ControlType.Button),
new PropertyCondition(
AutomationElement.ClassNameProperty,
"EdgeTabCloseButton")),
new AndCondition(
new PropertyCondition(
AutomationElement.ControlTypeProperty,
ControlType.Button),
new PropertyCondition(
AutomationElement.ClassNameProperty,
"TabCloseButton"))));
}
最後にタブを閉じます。見つかった「閉じるボタン」を擬似的にクリックするには、InvokePattern
というオブジェクトにキャストし、Invoke()
メソッドをコールすることで行います。
private static void CloseTab(AutomationElement tabItem)
{
AutomationElement? closeButton = FindCloseButton(tabItem);
if (closeButton == null)
{
return;
}
InvokePattern? invokePattern =
closeButton.GetCurrentPattern(InvokePattern.Pattern) as InvokePattern;
if (invokePattern == null)
{
return;
}
invokePattern.Invoke();
}
選択しなかった代替案
WebDriver
C#でEdgeのタブを閉じるにはどうしたらいいかChatGPTに聞くと、WebDriverの利用を進められました。確かにタブを閉じることはできたのですが、WebDriverが起動したEdgeしか操作することができませんでした。今回の要件では、ユーザーが手動で開いたEdgeも対象となっていました。もしかしたら何かやりようがあったかもしれませんが、私のスキルでは実現できず、WebDriverの利用は断念しました。
おわりに
この記事では、C#とUIAutomationを使ってMicrosoft Edgeの特定のタブを閉じる方法を紹介しました。参考になった場合は、ぜひコメントやフィードバックをお願いします。もっと良い方法や改善点があれば、教えていただけると幸いです。
参考文献
-
【Microsoft Learn】System.Windows.Automation 名前空間
UIAutomationのAPIリファレンス -
【Stack Overflow】How to close tabs on Microsoft Edge programmatically
Edgeのタブを閉じるプログラムのヒントになったディスカッション -
【</Sqripts>】UIオートメーション〜コピペで体験Windowsアプリの自動操作
理解につながったUIAutomationを使ったアプリの自動操作方法の解説記事