【C#】ディスプレイの特定の位置にブラウザを表示させるコード

# 概要
特定のアプリケーション内のボタン押下で、ディスプレイ画面の右部に「既定のブラウザ」で特定のURLのWebページを開く、という内容のサンプルコードを記載しました。
開く位置の指定が無い場合は簡単 (`Process.Start()`の引数にURLを渡すのみ？) かと思いますが、ブラウザを開く位置の指定があると少しややこしくなったので、備忘録として記載しました。

# 仕様 (やりたいこと)
- アプリケーション (以下、アプリA) 画面で特定のボタン (以下、ボタンA) を押下すると、Windows10の設定で「既定のブラウザ」として設定されているブラウザで特定のURLのWebページを開く
  - 「既定のブラウザ」がChrome、Edge (Chromium版) の場合
    - ブラウザを開く位置は、ディスプレイ画面の右側あたりに新しいウィンドウとして開く (全画面や、既に起動しているブラウザの新しいタブとしては開かない)
    
      <img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3533049/c20e06aa-b7ab-af54-6f05-b824b9d807c0.png" width="250">

    ※上の青枠がディスプレイ画面としたら、グレー部分のようにブラウザを開く
    - アプリモード (単独のアプリのように起動するモード) で開く
  - 「既定のブラウザ」がInternet Explorerの場合
    - Chrome、Edge (Chromium版) と同様の位置に開く
    - アプリモードは無い (と思われる) ので、アプリモードでは開かない
  - 「既定のブラウザ」がChrome、Edge (Chromium版) 、Internet Explorer以外の場合
    - 「既定のブラウザ」が開かれていれば新規タブで表示 (画面の位置やサイズは問わない)
    - 「既定のブラウザ」が開かれていなければ、「既定のブラウザ」を起動し表示 (画面の位置やサイズは問わない) 
- 「既定のブラウザ」が取得できなかった場合は、『「既定のブラウザ」がChrome、Edge (Chromium版) 、Internet Explorer以外の場合』と同様の処理で開く

### 注意点
- Chrome、Edge (Chromium版) 、IEで開く場合の位置は、シングルモニター (1画面)、デュアルモニター (2画面) どちらとも対応できるようにする
  - デュアルモニターの場合、アプリAの最大領域を保持しているディスプレイ画面の右側辺りにブラウザを開く

# コード

### Webページを開くクラス

```C#:WebPageOpener.cs
using Microsoft.Win32;
using System;
using System.Diagnostics;
using System.Runtime.InteropServices;
using System.Threading;
using System.Windows.Forms;
using System.Windows.Input;

namespace HogeNamespace
{
    /// <summary>
    /// Webページを開くためのクラス
    /// </summary>
    public class WebPageOpener
    {
        /// <summary>
        /// ウィンドウをフォアグラウンドに移動し、アクティブにする
        /// </summary>
        /// <param name="hWnd">ウィンドウのハンドル</param>
        /// <returns></returns>
        [DllImport("user32.dll")]
        private static extern bool SetForegroundWindow(IntPtr hWnd);

        // 既定のブラウザの設定値を格納しているレジストリのKey
        private static readonly string UserChoiceKey = @"Software\Microsoft\Windows\Shell\Associations\UrlAssociations\http\UserChoice";

        // Chromeのユーザープロファイルまでのパス
        private static readonly string ProfilePathForChrome = @"D:\Profiles\SampleWebPage\Chrome\User Data";

        // Edgeのユーザープロファイルまでのパス
        private static readonly string ProfilePathForEdge = @"D:\Profiles\SampleWebPage\Edge\User Data";

        // 各ブラウザのプログラム識別子
        private static readonly string ProgIdForChrome = "ChromeHTML";                      
        private static readonly string ProgIdForChromiumEdge = "MSEdgeHTM";  
        private static readonly string ProgIdForIE = "IE.HTTP"; 
        
        /// <summary>
        /// 表示するWebページのURL
        /// </summary>
        private readonly string _url;

        /// <summary>
        /// コンストラクタ
        /// </summary>
        /// <param name="url">表示するWebページのURL</param>
        public WebPageOpener(string url)
        {
            if (string.IsNullOrEmpty(url)) throw new ArgumentNullException("表示するWebページのURLを指定してください。");

            _url = url;
        }

        /// <summary>
        ///  Webページを表示する
        /// </summary>
        public void Execute()
        {
            Mouse.OverrideCursor = System.Windows.Input.Cursors.Wait;

            try
            {
                // Webページを表示
                ShowWebPage();
            }
            finally
            {
                Mouse.OverrideCursor = null;
            }
        }

        /// <summary>
        /// Webページを表示する
        /// </summary>
        /// <remarks>
        /// アプリが存在するモニタの右部に新規ウィンドウで表示する
        /// </remarks>
        private void ShowWebPage()
        {
            // ブラウザの表示領域を取得する
            var displayAreaInfo = GetDisplayArea();

            // 既定のブラウザを取得
            string progId = GetProgIdForDefaultBrowser();
            if (progId == null)
            {
                // 既定のブラウザを取得出来ない場合
                progId = "";
            }

            if (progId.StartsWith(ProgIdForChrome) || progId.StartsWith(ProgIdForChromiumEdge))
            {
                // Google Chrome または Edge(Chromium版)
                var psi = new ProcessStartInfo() { FileName = progId.StartsWith(ProgIdForChrome) ? "chrome" : "msedge" };
                string profilePath = progId.StartsWith(ProgIdForChrome) ?  ProfilePathForChrome: ProfilePathForEdge;               
                psi.Arguments = GetStartOptionForWebPage(displayAreaInfo, profilePath);

                // 開く
                Process.Start(psi);
            }
            else if (progId.StartsWith(ProgIdForIE))
            {
                // Internet Explorer
                ShowWebPageWithIE(displayAreaInfo);
            }
            else
            {
                // その他のブラウザ
                Process.Start(_url);
            }
        }

        /// <summary>
        /// ブラウザの表示領域を取得する
        /// </summary>
        /// <returns>ブラウザの表示領域</returns>
        private DisplayAreaInfo GetDisplayArea()
        {
            // アプリ画面が存在するディスプレイを取得
            var screen = GetTargetScreen();

            // ブラウザの表示領域を取得
            int screenTopPoint = screen.WorkingArea.Top;
            int screenLeftPoint = screen.WorkingArea.Left;
            int screenWidth = screen.WorkingArea.Width;
            int screenHeight = screen.WorkingArea.Height;
        
            var displayAreaInfo = new DisplayAreaInfo
            (
                // ディスプレイによって解像度が違うため、表示位置は割合で計算
                screenLeftPoint + screenWidth * 2 / 3,
                screenTopPoint + screenHeight / 40,
                screenWidth * 1 / 3,
                screenHeight * 35 / 40
            );

            return displayAreaInfo;
        }

        /// <summary>
        /// アプリの最大領域を保持しているディスプレイを取得する
        /// </summary>
        /// <returns>ディスプレイ情報</returns>
        private Screen GetTargetScreen()
        {
            // アプリのハンドルを取得
            // ハンドル取得方法はアプリによって色々あると思うのでここでは省略
            IntPtr handle = ハンドル取得処理;

           return Screen.FromHandle(handle);
        }

        /// <summary>
        /// レジストリから既定のブラウザのプログラム識別子を取得
        /// </summary>
        /// <returns>既定のブラウザのプログラム識別子</returns>
        private string GetProgIdForDefaultBrowser()
        {
            // UserChoiceの情報を取得 (読み取り専用で開く)
            var userChoiceKey = Registry.CurrentUser.OpenSubKey(UserChoiceKey, false);
            if (userChoiceKey == null)
            {
                // サブキーが存在しない場合
                // ※OpenSubKeyは指定したキーが存在しない場合はnullを返す
                return null;
            }

            // 既定のブラウザのプログラム識別子を取得
            string progId = (string)userChoiceKey.GetValue("ProgId", false);
            if (progId == null)
            {
               // ログ出力処理
               
                return null;
            }

            return progId;
        }

        /// <summary>
        /// Webページの起動オプションを取得
        /// </summary>
        /// <param name="displayAreaInfo">ブラウザの表示領域</param>
        /// <param name="profilePath">プロファイルのパス</param>
        /// <returns>起動オプション</returns>
        private string GetStartOptionForWebPage(DisplayAreaInfo displayAreaInfo, string profilePath)
        {
            string arguments = "--new-window";                                                           // 新規ウィンドウで開く
            arguments += $" --window-position={displayAreaInfo.LeftPoint},{displayAreaInfo.TopPoint}";   // ブラウザの表示位置
            arguments += $" --window-size={displayAreaInfo.Width},{displayAreaInfo.Height}";             // ブラウザの表示サイズ
            arguments += $" \"--user-data-dir={profilePath}\"";                                          // プロファイルを設定
            arguments += $" \"--app={_url}\"";                                                           // アプリモード

            return arguments;
        }

        /// <summary>
        /// Internet Explorerを使用してWebページを開く
        /// </summary>
        /// <param name="displayAreaInfo">ブラウザの表示領域</param>
        private void ShowWebPageWithIE(DisplayAreaInfo displayAreaInfo)
        {
            // COMオブジェクトを使用
            var objIE = new SHDocVw.InternetExplorer
            {
                Top = displayAreaInfo.TopPoint,
                Left = displayAreaInfo.LeftPoint,
                Width = displayAreaInfo.Width,
                Height = displayAreaInfo.Height
            };

            try
            {
                // 読み込み完了後にブラウザを表示する
                objIE.Navigate(_url);
                while (objIE.Busy || objIE.ReadyState != SHDocVw.tagREADYSTATE.READYSTATE_COMPLETE) Thread.Sleep(100);
                objIE.Visible = true;
                SetForegroundWindow((IntPtr)objIE.HWND);
            }
            finally
            {
                // 解放
                Marshal.FinalReleaseComObject(objIE);
                objIE = null;
            }
        }
    }
}

```
### ブラウザの表示領域格納クラス
```C#:DisplayAreaInfo.cs
    /// <summary>
    /// ブラウザの表示領域格納クラス
    /// </summary>
    private class DisplayAreaInfo
    {
        /// <summary>表示位置 (X座標) </summary>
        public int LeftPoint { get; }
        /// <summary>表示位置 (Y座標) </summary>
        public int TopPoint { get; }
        /// <summary>表示幅</summary>
        public int Width { get; }
        /// <summary>表示高さ</summary>
        public int Height { get; }

        /// <summary>
        /// コンストラクタ
        /// </summary>
        /// <param name="leftPoint">表示位置 (X座標) </param>
        /// <param name="topPoint">表示位置 (Y座標) </param>
        /// <param name="width">表示幅</param>
        /// <param name="height">表示高さ</param>
        public DisplayAreaInfo(int leftPoint, int topPoint, int width, int height)
        {
            LeftPoint = leftPoint;
            TopPoint = topPoint;
            Width = width;
            Height = height;
        }
    }
```

- `GetTargetScreen()`の`Screen.FromHandle(handle);`は表示されているアプリの最大領域を含んでいるディスプレイが返却されます。
- `GetStartOptionForWebPage()`の`--window-size`等の部分は、ブラウザを起動する際の起動オプションを指定しています。
  - 起動オプションは`ProcessStartInfo`クラスの`Arguments`に渡して指定できます。
  - 起動オプションを使用するにはユーザープロファイル (Profile Data) が必要のため、ユーザープロファイルを作成、使用しています。ただ普通にブラウジングする際のブラウザのユーザープロファイルは変更したくないため、別のユーザープロファイルを作成、使用しています。

### 呼出し方

```C#
    // アプリAのボタンA押下後の処理
    
    // ブラウザを起動しWebページを表示する
    string webPageUrl = "http://～";  // webページのURL
    var webPageOpener = new WebPageOpener(webPageUrl);
    webPageOpener.Execute();
```

# 注意点
「既定のブラウザ」がChromeもしくはEdge (Chromium版) の場合、以下の動作をすると、ブラウザが表示されるディスプレイが異なってしまいます:joy_cat:

デュアルモニタで「モニタ1」「モニタ2」がある状態で実施
1. アプリAの画面がモニタ1に存在する状態から、アプリAのボタンAを押下 (モニタ1にWebブラウザが表示される)
1. Webブラウザを開いたままの状態で、アプリAの画面をモニタ1からモニタ2に移動
1. アプリAのボタンAを押下 (モニタ1に再度Webブラウザが表示されてしまう)
※起動オプションよりブラウザで保持している位置情報が優先されてしまうためかと思われます。
※手順2と手順3の間に手順1で起動したブラウザを閉じていると、手順3では正常にモニタ2に表示されます。

# 終わりに
ブラウザの開く位置の指定があるだけで想像していたより複雑になったので少し驚きました。