-
[C#] - WinForm WebBrowser 사용방법 및 팁.NET/CSharp 2021. 12. 17. 20:15
C# WinForm WebBrowser 사용방법 및 팁
현재 회사에서 WebBrowser 컨트롤을 활용한 프로그램을 개발중에 있습니다. 개발하면서 겪었던 점을 정리합니다.
1.WebBrowser 레지스트리
Internet Feature Control Keys
https://msdn.microsoft.com/en-us/library/ee330720(v=vs.85).aspx
위에 링크는 WebBrowser를 활용하기 위해 필요한 기능(레지스트리 정보)에 대해서 설명하고 있습니다.
InternetFeatureControlKeys 클래스는 해당 레지스트리 정보들을 추가 할 수 있는 기능을 제공합니다.
InternetFeatureControlKeys.cs
using Microsoft.Win32; using System; using System.Collections.Generic; using System.Windows.Forms; namespace UseWebBrowser { public class InternetFeatureControlKeys { public const string FEATURE_BROWSER_EMULATION = "FEATURE_BROWSER_EMULATION"; /// <summary> /// 현재 프로세스가 64 비트 프로세스인지 여부 /// </summary> public static bool Is64BitProcess = IntPtr.Size == 8; public static bool Is64BitOperatingSystem = NativeMethods.IsOS64Bit(); /// <summary> /// FEATURE_BROWSER_EMULATION 값 자동적으로 설정 여부 /// </summary> public bool AutoBrowserEmulation { get { return _autoBrowserEmulation; } set { _autoBrowserEmulation = value; } } private bool _autoBrowserEmulation; /// <summary> /// FEATURE_BROWSER_EMULATION 값 /// </summary> public int BrowserEmulation { get { return _browserEmulation; } } private int _browserEmulation = 7000; /// <summary> /// 레지스트리 HKEY 타입 /// <para><see cref="HKEY.LocalMachine"/> 일때 관리자 권한 아닐 경우 예외발생</para> /// </summary> /// <exception cref="System.ArgumentException"></exception> public HKEY HKey { get { return _hKey; } set { if (value == HKEY.LocalMachine) { if (!Toolkit.IsExecuteAdministrator()) { throw new System.ArgumentException("Usage Execute Administrator"); } } _hKey = value; } } private HKEY _hKey = HKEY.CurrentUser; /// <summary> /// Internet Feature Control Keys /// <para>Key,Value</para> /// </summary> public Dictionary<string, uint> Features { get { return _features; } private set { _features = value; } } private Dictionary<string, uint> _features; public InternetFeatureControlKeys() { Features = new Dictionary<string, uint>(); SetBrowserEmulation(); } #region BrowserEmulation private int GetRegisterBrowserVersion() { string ieBrowserVersion = RegistryUtility.GetInternetExplorerVersion(); int indexOf = ieBrowserVersion.IndexOf('.'), result = 0; if (indexOf != -1) { int.TryParse(ieBrowserVersion.Substring(0, indexOf), out result); } return result; } private void SetWebBrowserCtrlVersion(int webBrowserMajorVersion, out int outWebBrowserCtrlVersion) { Toolkit.TraceWriteLine("WebBrowser Version=" + webBrowserMajorVersion); // Set the appropriate Internet Explorer version if (webBrowserMajorVersion >= 11) { outWebBrowserCtrlVersion = (int)BrowserEmulationVersion.Version11Edge; } else if (webBrowserMajorVersion == 10) { outWebBrowserCtrlVersion = (int)BrowserEmulationVersion.Version10Standards; } else if (webBrowserMajorVersion == 9) { outWebBrowserCtrlVersion = (int)BrowserEmulationVersion.Version9Standards; } else if (webBrowserMajorVersion == 8) { outWebBrowserCtrlVersion = (int)BrowserEmulationVersion.Version8Standards; } else { outWebBrowserCtrlVersion = (int)BrowserEmulationVersion.Version7; } Toolkit.TraceWriteLine("BrowserEmulationVersion=" + outWebBrowserCtrlVersion.ToString()); } private void SetBrowserEmulation() { Toolkit.TraceWriteLine("Start"); int webBrowserCtrlVersion = 0; using (WebBrowser Wb = new WebBrowser()) { SetWebBrowserCtrlVersion(Wb.Version.Major, out webBrowserCtrlVersion); } int.TryParse(webBrowserCtrlVersion.ToString().Substring(0, 2), out webBrowserCtrlVersion); int ieBrowserRegistryVersion = GetRegisterBrowserVersion(); Toolkit.TraceWriteLine("WebBrowser Version=" + webBrowserCtrlVersion); Toolkit.TraceWriteLine("InternetExplorer Registry Version=" + ieBrowserRegistryVersion); if (webBrowserCtrlVersion == ieBrowserRegistryVersion) { Toolkit.TraceWriteLine("Same WebBrowser Version == InternetExplorer Registry Version"); SetWebBrowserCtrlVersion(webBrowserCtrlVersion, out _browserEmulation); } else { Toolkit.TraceWriteLine("Same WebBrowser Version != InternetExplorer Registry Version"); Toolkit.TraceWriteLine(String.Format("WebBrowser Version={0}, InternetExplorer Registry Version={1}", webBrowserCtrlVersion, ieBrowserRegistryVersion)); SetWebBrowserCtrlVersion(ieBrowserRegistryVersion, out _browserEmulation); } Toolkit.TraceWriteLine("End"); } #endregion /// <summary> /// 레지스트리에 추가할 Internet Feature Control Key를 등록한다. /// </summary> /// <param name="key"></param> /// <param name="value"></param> public void AddFeature(string key, int value) { _features.Add(key, (uint)value); } /// <summary> /// appName 이름으로 Feature에 있는 내용을 레지스트리에 추가한다. /// </summary> /// <param name="appName"></param> public void WriteRegistry(string appName) { foreach (var item in _features) { SetRegistryBrowserFeatureControlKey( appName, item.Key, item.Value ); } if (_autoBrowserEmulation) { SetRegistryBrowserFeatureControlKey(appName, FEATURE_BROWSER_EMULATION, (uint)_browserEmulation); } } public void AddDefaultFeature() { // https://docs.microsoft.com/en-us/previous-versions/windows/internet-explorer/ie-developer/general-info/ee330720(v=vs.85) #region (A) 항목 #endregion #region (B..C) 항목 #endregion #region (D..H) 항목 #endregion #region (I..L) 항목 #endregion #region (M..R) 항목 #endregion #region (S..T) 항목 #endregion #region (U..Y) 항목 #endregion #region (Z) 항목 #endregion #region Obsolete Feature Controls #endregion // (B..C) 항목 if (!Features.ContainsKey("FEATURE_BROWSER_EMULATION")) { AddFeature("FEATURE_BROWSER_EMULATION", _browserEmulation); } // (U..Y) 항목 // (U..Y) 항목 foreach (var key in Features.Keys) { int value = (int)Features[key]; AddFeature(key, value); } } /// <summary> /// 레지스트리를 추가한다. /// <para>CurrentUser</para> /// <para>Software\Microsoft\Internet Explorer\Main\FeatureControl\</para> /// <para>LocalMachine</para> /// <para>x64비트 32비트 프로그램 = Software\Wow6432Node\Microsoft\Internet Explorer\Main\FeatureControl\</para> /// <para>그외 = Software\Microsoft\Internet Explorer\Main\FeatureControl\</para> /// </summary> /// <param name="appName"></param> /// <param name="feature"></param> /// <param name="value"></param> public void SetRegistryBrowserFeatureControlKey(string appName, string feature, uint value) { if (_hKey == HKEY.LocalMachine) { string subKey = String.Empty; if (!Is64BitProcess && Is64BitOperatingSystem) { subKey = @"Software\Wow6432Node\Microsoft\Internet Explorer\Main\FeatureControl\"; } else { subKey = @"Software\Microsoft\Internet Explorer\Main\FeatureControl\"; } subKey = String.Concat(subKey, feature); using (var key = Registry.LocalMachine.CreateSubKey(subKey, RegistryKeyPermissionCheck.ReadWriteSubTree)) { key.SetValue(appName, (UInt32)value, RegistryValueKind.DWord); } } else { string subKey = String.Concat(@"Software\Microsoft\Internet Explorer\Main\FeatureControl\", feature); using (var key = Registry.CurrentUser.CreateSubKey(subKey, RegistryKeyPermissionCheck.ReadWriteSubTree)) { key.SetValue(appName, (UInt32)value, RegistryValueKind.DWord); } } } } }
InternetFeatureControlKeys 클래스 생성시 HKEY 기본값이 CurrentUser 인걸 볼 수 있습니다. LocalMachine 인 경우에는 관리자권한 실행이 필요하고 32비트 64비트 일때 등록시키는 위치가 틀립니다. 자세한 내용은 시간이 날때 해당 포스트에 추가적으로 정리 할 예정입니다.
Wow6432 는 64비트 윈도우에서 32비트 프로그램의 레지스트리를 등록합니다.
32비트 윈도우에서 32비트 프로그램, 64비트 윈도우에서 64비트 프로그램
HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Internet Explorer\Main\FeatureControl\64비트 윈도우에서 32비트 프로그램
HKEY_LOCAL_MACHINE\SOFTWARE\Wow6432Node\Microsoft\Internet Explorer\Main\FeatureControl\Form Load 이벤트에서 WeBrowser 레지스트리 정보 적용
using System; using System.Collections.Generic; using System.ComponentModel; using System.Data; using System.Diagnostics; using System.Drawing; using System.IO; using System.Linq; using System.Reflection; using System.Text; using System.Threading.Tasks; using System.Windows.Forms; using System.Xml; namespace UseWebBrowser { public partial class Form : System.Windows.Forms.Form { public Form() { InitializeComponent(); } private void Form_Load(object sender, EventArgs e) { InternetFeatureControlKeys featureControlKeys = new InternetFeatureControlKeys(); Assembly assembly = System.Reflection.Assembly.GetEntryAssembly(); string path = Path.GetDirectoryName(assembly.Location) + Path.DirectorySeparatorChar + "InternetFeatureControlKeys.xml"; if (File.Exists(path)) { try { XmlDocument doc = new XmlDocument(); doc.Load(path); XmlNodeList xmlNodeList = doc.SelectNodes("InternetFeatureControlKeys/InternetFeature"); foreach (XmlNode xmlNode in xmlNodeList) { string name = (string)xmlNode.Attributes["name"].Value; if (name == InternetFeatureControlKeys.FEATURE_BROWSER_EMULATION && xmlNode.InnerText == "%Auto%") { featureControlKeys.AutoBrowserEmulation = true; continue; } uint value = (uint)int.Parse(xmlNode.InnerText); featureControlKeys.Features.Add(name, value); } } catch (Exception ex) { Toolkit.TraceWriteLine(ex.Message); Toolkit.TraceWriteLine(ex.StackTrace); featureControlKeys.AddDefaultFeature(); } } else { featureControlKeys.AddDefaultFeature(); } string appName = String.Empty; if (Debugger.IsAttached) { // Debug [F5 실행] Process process = Process.GetCurrentProcess(); appName = Path.GetFileName(process.MainModule.FileName); featureControlKeys.WriteRegistry(appName); Toolkit.DebugWriteLine("Debugger.IsAttached InternetFeatureControlKeys WriteRegistry appName - " + appName); } if (!appName.Equals(Application.ProductName + ".exe")) { // Execute appName = Application.ProductName + ".exe"; featureControlKeys.WriteRegistry(appName); Toolkit.TraceWriteLine("InternetFeatureControlKeys WriteRegistry appName - " + appName); } } } }
그림 2와 같이 Application.ProductName 값은 어셈블리 정보 창의 제품값에 해당됩니다. 만약에 해당값을 Entry 어셈블리명(실행 파일명 확장자 제외)과 틀릴 경우 문제가 될 수 있는 코드입니다. 그림2 같은 경우에는 어셈블리 명과 같아서 문제가 되질 않지만 문제가 될 만한 코드 이기 때문에 아래와 같이 Entry 어셈블리명.exe 로 추가적으로 레지스트리에 등록을 해줍니다.
if (!appName.Equals(Application.ProductName + ".exe")) { // Execute appName = Application.ProductName + ".exe"; featureControlKeys.WriteRegistry(appName); Toolkit.TraceWriteLine("InternetFeatureControlKeys WriteRegistry appName - " + appName); } appName = Assembly.GetEntryAssembly().GetName().Name; appName = appName + ".exe"; featureControlKeys.WriteRegistry(appName);
InternetFeatureControlKeys.xml
<?xml version="1.0" encoding="utf-8" ?> <!-- https://docs.microsoft.com/en-us/previous-versions/windows/internet-explorer/ie-developer/general-info/ee330720(v=vs.85) --> <InternetFeatureControlKeys> <!-- (A) 항목--> <!-- (B..C) 항목--> <!-- %Auto% 시 해당 값은 로컬 PC에 설치되어 있는 IE버전을 내부적으로 실행 시 설정 한다. --> <InternetFeature name="FEATURE_BROWSER_EMULATION">%Auto%</InternetFeature> <!-- (D..H) 항목--> <!-- (I..L) 항목 --> <!-- (M..R) 항목 --> <!-- (S..T) 항목 --> <!-- (U..Y) 항목 --> <!-- (Z) 항목 --> <!-- Obsolete Feature Controls --> </InternetFeatureControlKeys>
Form Load 이벤트에서는 InternetFeatureControlKeys.xml 파일을 읽어들여 해당 파일에 추가되어있는 정보들을 레지스트리에 추가하고 있습니다.
Visual Studio 출력
[Form :: Form_Load] DEBUG - Debugger.IsAttached InternetFeatureControlKeys WriteRegistry appName - UseWebBrowser.vshost.exe [Form :: Form_Load] TRACE - InternetFeatureControlKeys WriteRegistry appName - UseWebBrowser.exe
위와 같이 디버그 실행(F5), Ctrl + F5 실행 에 대한 파일 이름이 다르기 때문에 실행 파일 별로 레지스트리에 WebBrowser 레지스트리 정보를 추가하고 있습니다.
현재 그림 2와 같이 추가된 FEATURE_BROWSER_EMULATION 항목은 아래 내용을 참고하시기 바랍니다.
브라우저 버전 (FEATURE_BROWSER_EMULATION)
해당 값은 렌더링 되는 웹 페이지(JSP, HTML ..) 내용과 연관되어 있습니다. 자세한 내용은 추후 정리해보고자 합니다. content 값이 IE=edge 일 경우 11001로 설정해야합니다.
<meta http-equiv="X-UA-Compatible" content="IE=edge">
2.WebBrowser 에서 렌더링된 페이지와 데이터 통신
전달되는 데이터 형태는 프로그램 개발시에 유동적으로 바뀌기 때문에 하나의 JSON 객체의 문자열로 전달하는게 바람직합니다.
2-1. 렌더링된 페이지에서 WebBrowser 데이터 전달
렌더링된 페이지에서 WebBrowser 로 데이터 전달은 아래와 같이 전달합니다.
Test1.html
<!DOCTYPE html> <html lang="en" xmlns="http://www.w3.org/1999/xhtml"> <head> <meta charset="utf-8" /> <title>Test1</title> <script type="text/javascript"> function callNative() { if ("CallNative" in window.external) { var data = { eventName: "Btn1 OnClick" }; window.external.CallNative(JSON.stringify(data)); } } function receiveNative(arg) { alert(JSON.stringify(arg)); } </script> </head> <body> <div> <input type="button" id="Btn1" value="CallNative" onclick="callNative()"/> </div> </body> </html>
CallNative 함수는 C# 클라이언트 프로그램에 정의되어야 하는 메서드입니다. 해당 메서드는 WebBrowser.ObjectForScripting 속성에 설정되는 Object 에 정의되어야 하는 public 메서드이며 클래스 접근지정자 또한 public 이어야만 합니다. 그리고 해당 클래스에는 아래와 같이 어트리뷰트 추가가 필요합니다.
[PermissionSet(SecurityAction.Demand, Name = "FullTrust")] [ComVisible(true)]
되도록이면 해당 속성에 설정되는 클래스 객체는 별도의 클래스 (WebNativeManager)를 생성하여 작업하시기 바랍니다.
using System; using System.Collections.Generic; using System.Linq; using System.Runtime.InteropServices; using System.Security.Permissions; using System.Text; using System.Threading.Tasks; namespace UseWebBrowser { [PermissionSet(SecurityAction.Demand, Name = "FullTrust")] [ComVisible(true)] public class WebNativeManager { public void CallNative(object arg0) { } } }
그림 3과 같이 WebNativeManager 클래스 접근지정자가 public 이 아닐경우 System.ArgumentException 예외를 발생
그림 4와 같이 CallNative 메서드 접근지정자가 public 이 아닐경우 스크립트 오류 발생
WebNativeManager.WindowExternal.cs
using System; using System.Collections.Generic; using System.Linq; using System.Runtime.InteropServices; using System.Security.Permissions; using System.Text; using System.Threading.Tasks; using System.Windows.Forms; namespace UseWebBrowser { [PermissionSet(SecurityAction.Demand, Name = "FullTrust")] [ComVisible(true)] public partial class WebNativeManager { private WebBrowser _WebBrowser; public WebNativeManager(WebBrowser webBrowser) { _WebBrowser = webBrowser; } public void CallNative(object arg0) { Toolkit.TraceWriteLine("Arg0=" + arg0); } } }
Test1.html 페이지에서 WebBrowser 로 전달된 데이터
[WebNativeManager :: CallNative] TRACE - Arg0={"eventName":"Btn1 OnClick"}
CallNative 메서드가 try catch 문으로 예외처리가 되어 있지 않습니다. WebBrowser ScriptErrorsSuppressed 값이 False 일경우에는 에러를 확인 할 수 없는 상황이 오므로 반드시 try catch 문으로 감싸서 에러가 발생하였을 경우 로그처리를 해줘야 개발자가 의도치 않은 예외상황에 대처할 수 있습니다.
2-2. WebBrowser 에서 렌더링된 페이지로 데이터 전달
이번에는 반대로 WebBrowser 에서 렌더링된 페이지로 데이터를 전달합니다.
WebBrowser.Document 속성을 이용하여 해당 객체의 InvokeScript 메서드를 호출하여 렌더링된 페이지로 데이터를 전달 합니다.
WebNativeManager.InvokeScript.cs
using System; using System.Collections.Generic; using System.Linq; using System.Text; using System.Threading.Tasks; using System.Windows.Forms; namespace UseWebBrowser { partial class WebNativeManager { public object InvokeScript(string scriptName, object[] args) { return _WebBrowser.Document.InvokeScript(scriptName, args); } } }
3. 속성 및 이벤트
3-1. PreviewKeyDown 이벤트
private void Body_KeyDown(object sender, HtmlElementEventArgs e) { // 86 ASCII CODE = 'V' if (e.KeyPressedCode == 86 && e.CtrlKeyPressed) { string text = Clipboard.GetText(); _WebBrowser.Document.ExecCommand("Paste", false, text); } }
기존에 위와 같이 WebBrowser.Document.Body.KeyDown 이벤트로 Paste(Ctrl + V) 처리하려고 하였으나 e.KeyPressedCode 값이 제대로 넘어오질 않아 PreviewKeyDown 이벤트로 처리하였습니다.
해당 이벤트에서는 아래 코드가 정상적으로 동작을 합니다.
private void WebBrowser_PreviewKeyDown(object sender, PreviewKeyDownEventArgs e) { if (e.Control && e.KeyCode == Keys.V) { string text = Clipboard.GetText(); _WebBrowser.Document.ExecCommand("Paste", false, text); } else if (e.Control && e.KeyCode == Keys.A) { _WebBrowser.Document.ExecCommand("SelectAll", false, null); } else if (e.Control && e.KeyCode == Keys.C) { _WebBrowser.Document.ExecCommand("Copy", false, null); } else if (e.Control && e.KeyCode == Keys.X) { _WebBrowser.Document.ExecCommand("Cut", false, null); } if (e.KeyCode == Keys.Delete) { _WebBrowser.Document.ExecCommand("Delete", false, null); } }
4. 기타
4-1. WebBrowser 에서 HtmlElement로 마우스 이벤트 보내기
Test2.html
<!DOCTYPE html> <html> <head> <meta charset="utf-8"> <title>Test2</title> <script> var board = null; function bodyOnLoad() { alert("bodyOnLoad()"); board = document.getElementById("board"); board.onmousedown = function(e) { if (e) { // 웹 브라우저에서만 테스트 할 경우 호출 됨 alert("board.onmousedown(e)"); } else { alert("e undefined mousedown"); } }; } </script> <style> div { background-color:lightblue; width:5000px; height:5000px; } </style> </head> <body onload="bodyOnLoad()"> <div id="board"> </div> </body> </html>
WM_LBUTTONDOWN
private void ButtonLButtonDown_Click(object sender, EventArgs e) { if (_TextBoxPosX.Text.Length > 0 && _TextBoxPosY.Text.Length > 0) { int posX = int.Parse(_TextBoxPosX.Text); int posY = int.Parse(_TextBoxPosX.Text); IntPtr handle = NativeMethods.FindWindowEx(_WebBrowser.Handle, IntPtr.Zero, "Shell Embedding", ""); handle = NativeMethods.FindWindowEx(handle, IntPtr.Zero, "Shell DocObject View", ""); handle = NativeMethods.FindWindowEx(handle, IntPtr.Zero, "Internet Explorer_Server", ""); const int WM_LBUTTONDOWN = 0x0201; NativeMethods.PostMessage(handle, WM_LBUTTONDOWN, 0, NativeMethods.PointToLParam(posX, posY)); } }
그림 5를 보시면 ButtonLButtonDown_Click 이벤트 메서드에서 왜 핸들을 위와 같이 찾는지 이해가 되실거라고 생각되어 집니다. CEF(Chromium Embedded Framework) 브라우저 또한 위와 같은 방식으로 핸들을 찾아서 마우스 클릭없이 onmousedown 이벤트를 보낼 수 있습니다.
4-2. WebBrowser 컨트롤을 포함한 사용자 컨트롤 ActiveX
WebBrowser 컨트롤을 포함한 사용자 컨트롤을 ActiveX 로 만들어 InternetExplorer 브라우저에 Embedd 하려고 하였으나 ObjectDisposeException 발생
그리하여 MFC ActiveX 컨트롤에 해당 사용자 컨트롤을 래핑하여 올렸다.
(해당 개념에 대해서는 CCW(COM Callable Wrapper) 를 찾아보시기 바랍니다.)4-3. WebBrowser 컨트롤에서 렌더링한 페이지 개발자 도구 열기
해당 내용은 최근에서야 알게 되었으며 아래 링크를 통하여 확인 할 수 있습니다.
https://docs.microsoft.com/ko-kr/microsoft-edge/devtools-guide-chromium/ie-mode/
그림 6과 같이 실행 창에서 IEChooser.exe 를 실행하면 그림 7과 같은 화면이 나오게 되는데 브라우저 컨트롤을 활용한 프로그램이 실행중일 경우 디버그할 대상에 목록이 나타나게 됩니다.
자세하게 어떠한 환경에서 IEChooser.exe 가 설치되어 있으며 동작하게 되는지는 추후에 연구해 포스트에 추가하도록 하겠습니다.
5. mshtml(Microsoft HTML Object Library)
WebBrowser 컨트롤의 코어 기능을 사용하기 위해서는 mshtml.dll 을 활용하여 처리 할 수 있습니다. 프로젝트/참조 참조추가 메뉴를 클릭하여 COM 항목에서 Microsoft HTML Object Library 항목을 체크하여 추가합니다.
5.1 Ctrl + MouseWheel 시 확대/축소 방지하기
참고
https://stackoverflow.com/questions/3971675/c-sharp-capturing-ctrl-mouse-wheel-in-webbrowser-control
GitHub Source
https://github.com/soultomind/Blog/tree/develop/UseWebBrowser'.NET > CSharp' 카테고리의 다른 글
[C#] - Process.MainWindowHandle 속성 (0) 2022.05.25 [C#] - WinForm Edge WebView2 사용방법 및 팁 (0) 2022.03.24 [C#] - WinForm 디자인모드 사용방법 및 팁 (0) 2021.10.19 [C#] - Settings.Default.Save() 시 user.config 저장 위치 (0) 2020.11.02 [C#] - .NET Framework 3.5 TLS 1.2 적용하기 (0) 2020.06.15