0
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

【C#】UIAutomationを利用してMicrosoft Edgeのタブを閉じる

Last updated at Posted at 2024-08-23

はじめに

業務アプリケーションの開発において、ユーザーがログアウトする際にMicrosoft Edgeで開いている特定のWebページだけを自動で閉じる必要が出てきました。Edge全体のプロセスを終了させる方法も考えましたが、閉じたくないWebページもあり、特定のタブのみを対象にする必要がありました。

本記事では、C#とUIAutomationを利用して、指定したタブだけを閉じる方法を記します。ここで紹介するコードは、2024年8月時点で動作確認したものとなります。

目次

やりたいこと

この状態から、タブのタイトルである「Google」という文字列を指定しタブを閉じて、
before.png

こうしたい。
after.png

環境

  • 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を直接参照することで利用できるようになります。

reference.png

パスは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のタブを閉じる処理を持つクラスのコードを示します。

EdgeAutomation.cs
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の特定のタブを閉じる方法を紹介しました。参考になった場合は、ぜひコメントやフィードバックをお願いします。もっと良い方法や改善点があれば、教えていただけると幸いです。

参考文献

0
1
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?