.NET Framework Interop (CLR, C#, VB)

Post your working scripts, libraries and tools for AHK v1.1 and older
User avatar
dd900
Posts: 121
Joined: 27 Oct 2013, 16:03

Re: .NET Framework Interop (CLR, C#, VB)

16 Dec 2021, 20:43

Here is an easy way to communicate with your ahk script from C#. I'm using .Net 4.8 with C# 9

Add class AhkMessenger to your c# project, or add the relevant code to a class you already have
C#

For the ahk setup you add a hidden button and an edit to your existing gui
AHK

Alternatively you could wrap a gui in a class to be the message reciever
AHK
User avatar
Shield
Posts: 9
Joined: 07 Dec 2021, 17:55

Re: .NET Framework Interop (CLR, C#, VB)

22 Oct 2022, 08:19

Hello, I'm struggling for weeks now to readout an external applications custom control which consists of rows and columns. While ControlGet can't access it at all, both MSAA and UIA see it as a List, feeding the Description property of the leading column with the content of the remaining cells, however, the result happens to be incomplete at times. My last hope now is FlaUI, which in UIA2 mode uses objects defined in the .NET Framework’s Automation namespace. Here the control is not seen as a List but as a DataGrid, granting direct access to the cells.

FLAUInspect v1.2.0 (https://github.com/FlaUI/FlaUInspect/releases/tag/v1.2.0) works fine even on Window 7, so the included assemblies are the ones I'm targeting. You may want to take a closer look at the source code here: https://www.fuget.org/packages/FlaUI.Core/1.1.0/lib/net45/FlaUI.Core.dll/FlaUI.Core/Application?code=true

@malcev kindly pointed me to CLR.ahk, but I'm having a hard time to get things going. This is simply over my head. After hours of trial and error I am unable to even run Notepad by means of FlaUI.Core.dll:

Code: Select all

; Using CLR.ahk v1.2
; Requires FlaUI.Core.dll of FlaUInspect v1.2

IF !CLR_Start()
	{	MsgBox Failed to start CLR.`n`nExiting.
		ExitApp
	}

UIACore_dll := CLR_LoadLibrary("FlaUI.Core.dll")
If !IsObject(UIACore_dll)
	{	MsgBox Loading FlaUI.Core.dll failed.`n`nExiting.
		ExitApp
	}

; CLR_CreateObject(UIACore_dll, "FlaUI.Core.Application") seems to be the right thing to do here, but fails. This doesn't:
ApplicationObj := CLR_CreateObject(UIACore_dll, "FlaUI.Core.Application", ComObject(3,0), ComObject(0xB, false))
If !IsObject(ApplicationObj)
	{	MsgBox CLR_CreateObject failed.`n`nExiting.
		ExitApp
	}

; ... but this leads to Error 0x80020006 (DISP_E_UNKNOWNNAME)
ApplicationObj.Launch("C:\Windows\System32\Notepad.exe")

ExitApp

; ==== CLR.ahk v1.2 by Lexikos  ====

CLR_LoadLibrary(AssemblyName, AppDomain=0)
{
	if !AppDomain
		AppDomain := CLR_GetDefaultDomain()
	e := ComObjError(0)
	Loop 1 {
		if assembly := AppDomain.Load_2(AssemblyName)
			break
		static null := ComObject(13,0)
		args := ComObjArray(0xC, 1),  args[0] := AssemblyName
		typeofAssembly := AppDomain.GetType().Assembly.GetType()
		if assembly := typeofAssembly.InvokeMember_3("LoadWithPartialName", 0x158, null, null, args)
			break
		if assembly := typeofAssembly.InvokeMember_3("LoadFrom", 0x158, null, null, args)
			break
	}
	ComObjError(e)
	return assembly
}

CLR_CreateObject(Assembly, TypeName, Args*)
{
	if !(argCount := Args.MaxIndex())
		return Assembly.CreateInstance_2(TypeName, true)
	
	vargs := ComObjArray(0xC, argCount)
	Loop % argCount
		vargs[A_Index-1] := Args[A_Index]
	
	static Array_Empty := ComObjArray(0xC,0), null := ComObject(13,0)
	
	return Assembly.CreateInstance_3(TypeName, true, 0, null, vargs, null, Array_Empty)
}

CLR_CompileC#(Code, References="", AppDomain=0, FileName="", CompilerOptions="")
{
	return CLR_CompileAssembly(Code, References, "System", "Microsoft.CSharp.CSharpCodeProvider", AppDomain, FileName, CompilerOptions)
}

CLR_CompileVB(Code, References="", AppDomain=0, FileName="", CompilerOptions="")
{
	return CLR_CompileAssembly(Code, References, "System", "Microsoft.VisualBasic.VBCodeProvider", AppDomain, FileName, CompilerOptions)
}

CLR_StartDomain(ByRef AppDomain, BaseDirectory="")
{
	static null := ComObject(13,0)
	args := ComObjArray(0xC, 5), args[0] := "", args[2] := BaseDirectory, args[4] := ComObject(0xB,false)
	AppDomain := CLR_GetDefaultDomain().GetType().InvokeMember_3("CreateDomain", 0x158, null, null, args)
	return A_LastError >= 0
}

CLR_StopDomain(ByRef AppDomain)
{	; ICorRuntimeHost::UnloadDomain
	DllCall("SetLastError", "uint", hr := DllCall(NumGet(NumGet(0+RtHst:=CLR_Start())+20*A_PtrSize), "ptr", RtHst, "ptr", ComObjValue(AppDomain))), AppDomain := ""
	return hr >= 0
}

; NOTE: IT IS NOT NECESSARY TO CALL THIS FUNCTION unless you need to load a specific version.
CLR_Start(Version="") ; returns ICorRuntimeHost*
{
	static RtHst := 0
	; The simple method gives no control over versioning, and seems to load .NET v2 even when v4 is present:
	; return RtHst ? RtHst : (RtHst:=COM_CreateObject("CLRMetaData.CorRuntimeHost","{CB2F6722-AB3A-11D2-9C40-00C04FA30A3E}"), DllCall(NumGet(NumGet(RtHst+0)+40),"uint",RtHst))
	if RtHst
		return RtHst
	EnvGet SystemRoot, SystemRoot
	if Version =
		Loop % SystemRoot "\Microsoft.NET\Framework" (A_PtrSize=8?"64":"") "\*", 2
			if (FileExist(A_LoopFileFullPath "\mscorlib.dll") && A_LoopFileName > Version)
				Version := A_LoopFileName
	if DllCall("mscoree\CorBindToRuntimeEx", "wstr", Version, "ptr", 0, "uint", 0
	, "ptr", CLR_GUID(CLSID_CorRuntimeHost, "{CB2F6723-AB3A-11D2-9C40-00C04FA30A3E}")
	, "ptr", CLR_GUID(IID_ICorRuntimeHost,  "{CB2F6722-AB3A-11D2-9C40-00C04FA30A3E}")
	, "ptr*", RtHst) >= 0
		DllCall(NumGet(NumGet(RtHst+0)+10*A_PtrSize), "ptr", RtHst) ; Start
	return RtHst
}

;
; INTERNAL FUNCTIONS
;

CLR_GetDefaultDomain()
{
	static defaultDomain := 0
	if !defaultDomain
	{	; ICorRuntimeHost::GetDefaultDomain
		if DllCall(NumGet(NumGet(0+RtHst:=CLR_Start())+13*A_PtrSize), "ptr", RtHst, "ptr*", p:=0) >= 0
			defaultDomain := ComObject(p), ObjRelease(p)
	}
	return defaultDomain
}

CLR_CompileAssembly(Code, References, ProviderAssembly, ProviderType, AppDomain=0, FileName="", CompilerOptions="")
{
	if !AppDomain
		AppDomain := CLR_GetDefaultDomain()
	
	if !(asmProvider := CLR_LoadLibrary(ProviderAssembly, AppDomain))
	|| !(codeProvider := asmProvider.CreateInstance(ProviderType))
	|| !(codeCompiler := codeProvider.CreateCompiler())
		return 0

	if !(asmSystem := (ProviderAssembly="System") ? asmProvider : CLR_LoadLibrary("System", AppDomain))
		return 0
	
	; Convert | delimited list of references into an array.
	StringSplit, Refs, References, |, %A_Space%%A_Tab%
	aRefs := ComObjArray(8, Refs0)
	Loop % Refs0
		aRefs[A_Index-1] := Refs%A_Index%
	
	; Set parameters for compiler.
	prms := CLR_CreateObject(asmSystem, "System.CodeDom.Compiler.CompilerParameters", aRefs)
	, prms.OutputAssembly          := FileName
	, prms.GenerateInMemory        := FileName=""
	, prms.GenerateExecutable      := SubStr(FileName,-3)=".exe"
	, prms.CompilerOptions         := CompilerOptions
	, prms.IncludeDebugInformation := true
	
	; Compile!
	compilerRes := codeCompiler.CompileAssemblyFromSource(prms, Code)
	
	if error_count := (errors := compilerRes.Errors).Count
	{
		error_text := ""
		Loop % error_count
			error_text .= ((e := errors.Item[A_Index-1]).IsWarning ? "Warning " : "Error ") . e.ErrorNumber " on line " e.Line ": " e.ErrorText "`n`n"
		MsgBox, 16, Compilation Failed, %error_text%
		return 0
	}
	; Success. Return Assembly object or path.
	return compilerRes[FileName="" ? "CompiledAssembly" : "PathToAssembly"]
}

CLR_GUID(ByRef GUID, sGUID)
{
	VarSetCapacity(GUID, 16, 0)
	return DllCall("ole32\CLSIDFromString", "wstr", sGUID, "ptr", &GUID) >= 0 ? &GUID : ""
}

My nerves are quite frayed, I really need help with this / a working example i can build upon.
malcev
Posts: 1769
Joined: 12 Aug 2014, 12:37

Re: .NET Framework Interop (CLR, C#, VB)

22 Oct 2022, 09:07

I didnot try to use FlaUI.Core.dll:, but You can use UIDeskAutomation like this
http://automationspy.freecluster.eu/#uideskautomation

Code: Select all

csharp=
(
using UIDeskAutomationLib;
using System.Windows.Forms;
class Program
{
   static void Main(string[] args)
   {
      var engine = new Engine();
      engine.StartProcess("C:\\Windows\\System32\\Notepad.exe");
      UIDA_Window window = engine.GetTopLevel("Untitled - Notepad");
      string message = window.GetWindowText(); 
      MessageBox.Show(message); 
   }
}
)

CLR_CompileC#(csharp,"UiDeskAutomation.dll|System.Windows.Forms.dll", 0, "UIA.exe", "/target:winexe")
malcev
Posts: 1769
Joined: 12 Aug 2014, 12:37

Re: .NET Framework Interop (CLR, C#, VB)

22 Oct 2022, 10:16

Also You can easy run FlaUI from powershell:
https://github.com/FlaUI/FlaUI/wiki/FlaUI-in-PowerShell

Code: Select all

FlaUI_Core_dll := A_ScriptDir "\FlaUI.Core.dll"
FlaUI_UIA3_dll := A_ScriptDir "\FlaUI.UIA3.dll"
Interop_UIAutomationClient_dll := A_ScriptDir "\Interop.UIAutomationClient.dll"

cSharp =
(
using System.Text;
using System.Collections.ObjectModel;
using System.Management.Automation;
using System.Management.Automation.Runspaces;

public class ps
{
	public string RunScript(string scriptText)
	{
		Runspace runspace = RunspaceFactory.CreateRunspace();
		runspace.Open();

		Pipeline pipeline = runspace.CreatePipeline();
		pipeline.Commands.AddScript(scriptText);
		pipeline.Commands.Add("Out-String");

		Collection<PSObject> results = pipeline.Invoke();

		runspace.Close();
		return results[0].ToString();
		
		/*
		StringBuilder stringBuilder = new StringBuilder();
		foreach (PSObject obj in results)
		{
			stringBuilder.AppendLine(obj.ToString());
		}

		return stringBuilder.ToString();
		*/
	}
}
)

asm := CLR_CompileC#( cSharp, "System.Core.dll | C:\Windows\Microsoft.NET\assembly\GAC_MSIL\System.Management.Automation\v4.0_3.0.0.0__31bf3856ad364e35\System.Management.Automation.dll" )
ps := asm.CreateInstance("ps")
psCode =
(
[Reflection.Assembly]::LoadFile("%FlaUI_Core_dll%") | Out-Null
[Reflection.Assembly]::LoadFile("%FlaUI_UIA3_dll%") | Out-Null
[Reflection.Assembly]::LoadFile("%Interop_UIAutomationClient_dll%") | Out-Null

$app = [FlaUI.Core.Application]::Launch('notepad')
$uia = New-Object FlaUI.UIA3.UIA3Automation
$mw = $app.GetMainWindow($uia)
$a = $mw.Title
$title = $mw.FindFirstChild($uia.ConditionFactory.ByControlType([FlaUI.Core.Definitions.ControlType]::TitleBar))
$buttons = $title.FindAllChildren($uia.ConditionFactory.ByControlType([FlaUI.Core.Definitions.ControlType]::Button))
$closeButton = $buttons[2].AsButton()
$closeButton.Invoke()
$uia.Dispose()
$app.Dispose()
return $a
)
msgbox % ps.RunScript(psCode)
User avatar
Shield
Posts: 9
Joined: 07 Dec 2021, 17:55

Re: .NET Framework Interop (CLR, C#, VB)

22 Oct 2022, 14:16

Hello @malcev. Thank you (again) for adressing my problem, and providing those exemples. UIDeskAutomation indeed looks promising as is grants direct access to the cells, just like FlaUI and UISpy do.

I should mention that the readout of this control is just a small but important feature of a bigger AHK project. Clicking that grid control triggers a context menu from which the user can pick selected contents, and which should appear without delay. I've already finished that, It's all there, thanks to the Acc library (viewtopic.php?t=26201). Then I became aware of that issue with the final character missing sometimes.
:headwall:
Anyway, compling C# code into some external executable in order to achieve the same goal seems awkward, slowing down and overcomplicating things. Besides, I am completely unfamiliar with C#. And the generated UIA.exe from your example #1 crashes a few seconds after opening Notepad. Many reasons why I prefer to call the methods that FlaUI or UIDeskAutomation provide directly from within the AHK project. That's why I need to know why the code that I provided does not work.
malcev
Posts: 1769
Joined: 12 Aug 2014, 12:37

Re: .NET Framework Interop (CLR, C#, VB)

22 Oct 2022, 19:13

To use static methods You need to compile c# code.
Or something like this

Code: Select all

asm := CLR_LoadLibrary(A_ScriptDir "\FlaUI.Core.dll")
Launch(asm, "notepad.exe")
return

Launch(asm, executable)
{
   args := ComObjArray(0xC, 1), args[0] := executable
   asm.GetType_2("FlaUI.Core.Application").InvokeMember_3("Launch", 0x158, ComObject(13,0), ComObject(13,0), args)
}
If You dont know c# it is easier just run ready code through powershell.
lexikos
Posts: 9593
Joined: 30 Sep 2013, 04:07
Contact:

Re: .NET Framework Interop (CLR, C#, VB)

22 Oct 2022, 20:35

Shield wrote:
22 Oct 2022, 14:16
Anyway, compling C# code into some external executable in order to achieve the same goal seems awkward, slowing down and overcomplicating things.
If you omit the last two parameters from malcev's example, CLR_CompileC# will compile the code, load the assembly into memory and return a reference. You can define a class in the C# code and then instantiate it via the returned Assembly, giving you an object to interface between AutoHotkey and C#. This is how it is usually done with CLR.ahk.

I doubt that these .NET libraries can do anything unique with regard to automation of external processes. Automation relies on certain frameworks being in place. Unless the target application is specifically designed to provide an interface to other .NET applications, there's unlikely to be anything you can do from .NET that you can't do from native code (and therefore AutoHotkey).
[FlaUI] is based on native UI Automation libraries from Microsoft
Source: FlaUI/FlaUI: UI automation library for .Net
[UIDeskAutomation] is created on top of managed Microsoft UI Automation API.
Source: ddeltasolutions/UIDeskAutomation
You could just use UI Automation directly from AutoHotkey (I suggest searching the forum).
malcev
Posts: 1769
Joined: 12 Aug 2014, 12:37

Re: .NET Framework Interop (CLR, C#, VB)

22 Oct 2022, 22:00

I doubt that these .NET libraries can do anything unique with regard to automation of external processes.
Here was founded
I looked a bit into UISpy, which seems to be a deprecated .NET version of Inspect. But it does seem to be able to access more information that Inspect. I didn't use UISpy (had some difficulty running it), but instead used FlaUI inspect tool: in managed UIA3 mode it showed pretty much the same stuff as Inspect, but in unmanaged UIA2 mode it showed the Grid pattern.
viewtopic.php?p=486996#p486996
If compiling: FlaUI.Core.dll need to put at autohotkey installed folder (dll should be at the same folder as exe that runs script).

Code: Select all

cSharp =
(
   using System;
   using FlaUI.Core;
   class foo {
        public void Test() {
            FlaUI.Core.Application.Launch("calc.exe");
        }
   }
)
asm := CLR_CompileC#(cSharp, "System.dll | C:\Program Files\AutoHotkey\FlaUI.Core.dll")
obj := CLR_CreateObject(asm, "foo")
obj.Test()
return
User avatar
Shield
Posts: 9
Joined: 07 Dec 2021, 17:55

Re: .NET Framework Interop (CLR, C#, VB)

23 Oct 2022, 10:49

malcev wrote:
22 Oct 2022, 19:13
To use static methods You need to compile c# code.
Or something like this

Code: Select all

asm := CLR_LoadLibrary(A_ScriptDir "\FlaUI.Core.dll")
Launch(asm, "notepad.exe")
return

Launch(asm, executable)
{
   args := ComObjArray(0xC, 1), args[0] := executable
   asm.GetType_2("FlaUI.Core.Application").InvokeMember_3("Launch", 0x158, ComObject(13,0), ComObject(13,0), args)
}
@malcev: I think this exactly suits my needs! I modified the code in order to invoke the FromPoint method which resides in FlaUI.UIA2.dll, but for some strange reason that method isn't found:

Code: Select all

; Using CLR.ahk v1.2
; Requires FlaUI.UIA2.dll of FlaUInspect v1.2 in A_ScriptDir

If FileExist(A_ScriptDir "\FlaUI.UIA2.dll")
	{
		ui2 := CLR_LoadLibrary(A_ScriptDir "\FlaUI.UIA2.dll")
		MsgBox, % IsObject(GetElementFromPoint(ui2)) ? "Success" : "Failure"
	}
else
	MsgBox FlaUI.UIA2.dll not found in A_ScriptDir.`n`nExiting.

ExitApp

; Trying to get the automation element under the mouse cursor

GetElementFromPoint(ui2)
	{
		VarSetCapacity(POINT, 8, 0)
		DllCall("GetCursorPos", "ptr", &POINT)

		ComObjArray(0xC, 1)	; Probably needs to be adusted (to A_PtrSize?) since we need to place a pointer here
		args[0] := &Point

		; Some homework I have done in order to understand things:
		; InvokeMember_3(BSTR name, BindingFlags invokeAttr, IBinder *Binder, VARIANT Target, SAFEARRAY args, VARIANT *pRetVal);
		;	"A BSTR is a composite data type that consists of a length prefix, a data string, and a terminator."
		;	BindingFlags: InvokeMethod 0x100 + FlattenHierarchy 0x40 + NonPublic 0x20 + Public 0x10 + Static 0x08 + Instance 0x04
		;	ComObject(13,0) = NULL parameter

		AutomationElement := ui2.GetType_2("FlaUI.UIA2.UIA2Automation").InvokeMember_3("FromPoint", 0x17C, ComObject(13,0), ComObject(13,0), args)
		Return % AutomationElement
	}

Esc::
ExitApp

; ==== CLR.ahk v1.2 by Lexikos  ====

CLR_LoadLibrary(AssemblyName, AppDomain=0)
{
	if !AppDomain
		AppDomain := CLR_GetDefaultDomain()
	e := ComObjError(0)
	Loop 1 {
		if assembly := AppDomain.Load_2(AssemblyName)
			break
		static null := ComObject(13,0)
		args := ComObjArray(0xC, 1),  args[0] := AssemblyName
		typeofAssembly := AppDomain.GetType().Assembly.GetType()
		if assembly := typeofAssembly.InvokeMember_3("LoadWithPartialName", 0x158, null, null, args)
			break
		if assembly := typeofAssembly.InvokeMember_3("LoadFrom", 0x158, null, null, args)
			break
	}
	ComObjError(e)
	return assembly
}

CLR_CreateObject(Assembly, TypeName, Args*)
{
	if !(argCount := Args.MaxIndex())
		return Assembly.CreateInstance_2(TypeName, true)
	
	vargs := ComObjArray(0xC, argCount)
	Loop % argCount
		vargs[A_Index-1] := Args[A_Index]
	
	static Array_Empty := ComObjArray(0xC,0), null := ComObject(13,0)
	
	return Assembly.CreateInstance_3(TypeName, true, 0, null, vargs, null, Array_Empty)
}

CLR_CompileC#(Code, References="", AppDomain=0, FileName="", CompilerOptions="")
{
	return CLR_CompileAssembly(Code, References, "System", "Microsoft.CSharp.CSharpCodeProvider", AppDomain, FileName, CompilerOptions)
}

CLR_CompileVB(Code, References="", AppDomain=0, FileName="", CompilerOptions="")
{
	return CLR_CompileAssembly(Code, References, "System", "Microsoft.VisualBasic.VBCodeProvider", AppDomain, FileName, CompilerOptions)
}

CLR_StartDomain(ByRef AppDomain, BaseDirectory="")
{
	static null := ComObject(13,0)
	args := ComObjArray(0xC, 5), args[0] := "", args[2] := BaseDirectory, args[4] := ComObject(0xB,false)
	AppDomain := CLR_GetDefaultDomain().GetType().InvokeMember_3("CreateDomain", 0x158, null, null, args)
	return A_LastError >= 0
}

CLR_StopDomain(ByRef AppDomain)
{	; ICorRuntimeHost::UnloadDomain
	DllCall("SetLastError", "uint", hr := DllCall(NumGet(NumGet(0+RtHst:=CLR_Start())+20*A_PtrSize), "ptr", RtHst, "ptr", ComObjValue(AppDomain))), AppDomain := ""
	return hr >= 0
}

; NOTE: IT IS NOT NECESSARY TO CALL THIS FUNCTION unless you need to load a specific version.
CLR_Start(Version="") ; returns ICorRuntimeHost*
{
	static RtHst := 0
	; The simple method gives no control over versioning, and seems to load .NET v2 even when v4 is present:
	; return RtHst ? RtHst : (RtHst:=COM_CreateObject("CLRMetaData.CorRuntimeHost","{CB2F6722-AB3A-11D2-9C40-00C04FA30A3E}"), DllCall(NumGet(NumGet(RtHst+0)+40),"uint",RtHst))
	if RtHst
		return RtHst
	EnvGet SystemRoot, SystemRoot
	if Version =
		Loop % SystemRoot "\Microsoft.NET\Framework" (A_PtrSize=8?"64":"") "\*", 2
			if (FileExist(A_LoopFileFullPath "\mscorlib.dll") && A_LoopFileName > Version)
				Version := A_LoopFileName
	if DllCall("mscoree\CorBindToRuntimeEx", "wstr", Version, "ptr", 0, "uint", 0
	, "ptr", CLR_GUID(CLSID_CorRuntimeHost, "{CB2F6723-AB3A-11D2-9C40-00C04FA30A3E}")
	, "ptr", CLR_GUID(IID_ICorRuntimeHost,  "{CB2F6722-AB3A-11D2-9C40-00C04FA30A3E}")
	, "ptr*", RtHst) >= 0
		DllCall(NumGet(NumGet(RtHst+0)+10*A_PtrSize), "ptr", RtHst) ; Start
	return RtHst
}

;
; INTERNAL FUNCTIONS
;

CLR_GetDefaultDomain()
{
	static defaultDomain := 0
	if !defaultDomain
	{	; ICorRuntimeHost::GetDefaultDomain
		if DllCall(NumGet(NumGet(0+RtHst:=CLR_Start())+13*A_PtrSize), "ptr", RtHst, "ptr*", p:=0) >= 0
			defaultDomain := ComObject(p), ObjRelease(p)
	}
	return defaultDomain
}

CLR_CompileAssembly(Code, References, ProviderAssembly, ProviderType, AppDomain=0, FileName="", CompilerOptions="")
{
	if !AppDomain
		AppDomain := CLR_GetDefaultDomain()
	
	if !(asmProvider := CLR_LoadLibrary(ProviderAssembly, AppDomain))
	|| !(codeProvider := asmProvider.CreateInstance(ProviderType))
	|| !(codeCompiler := codeProvider.CreateCompiler())
		return 0

	if !(asmSystem := (ProviderAssembly="System") ? asmProvider : CLR_LoadLibrary("System", AppDomain))
		return 0
	
	; Convert | delimited list of references into an array.
	StringSplit, Refs, References, |, %A_Space%%A_Tab%
	aRefs := ComObjArray(8, Refs0)
	Loop % Refs0
		aRefs[A_Index-1] := Refs%A_Index%
	
	; Set parameters for compiler.
	prms := CLR_CreateObject(asmSystem, "System.CodeDom.Compiler.CompilerParameters", aRefs)
	, prms.OutputAssembly          := FileName
	, prms.GenerateInMemory        := FileName=""
	, prms.GenerateExecutable      := SubStr(FileName,-3)=".exe"
	, prms.CompilerOptions         := CompilerOptions
	, prms.IncludeDebugInformation := true
	
	; Compile!
	compilerRes := codeCompiler.CompileAssemblyFromSource(prms, Code)
	
	if error_count := (errors := compilerRes.Errors).Count
	{
		error_text := ""
		Loop % error_count
			error_text .= ((e := errors.Item[A_Index-1]).IsWarning ? "Warning " : "Error ") . e.ErrorNumber " on line " e.Line ": " e.ErrorText "`n`n"
		MsgBox, 16, Compilation Failed, %error_text%
		return 0
	}
	; Success. Return Assembly object or path.
	return compilerRes[FileName="" ? "CompiledAssembly" : "PathToAssembly"]
}

CLR_GUID(ByRef GUID, sGUID)
{
	VarSetCapacity(GUID, 16, 0)
	return DllCall("ole32\CLSIDFromString", "wstr", sGUID, "ptr", &GUID) >= 0 ? &GUID : ""
}
I wonder if this is due to the override modifier that I noticed within the source code.

@lexikos: The FromPoint method is somewhat special as its sole argument is a pointer (to a POINT structure). Which VarType the SafeArray needs to be in this case?
lexikos
Posts: 9593
Joined: 30 Sep 2013, 04:07
Contact:

Re: .NET Framework Interop (CLR, C#, VB)

24 Oct 2022, 02:32

Shield wrote:The FromPoint method is somewhat special as its sole argument is a pointer (to a POINT structure).
No, the method takes a Point struct by value. Managed APIs very rarely deal with pointers, and when they do, the API is marked unsafe (unless the API accepts it as a plain integer, IntPtr). A parameter of type ref Point would correspond to a pointer to a Point in P/Invoke or a COM Callable Wrapper.

IDispatch does not support structs. It may be possible to call this through the native COM Callable Wrapper for the object using DllCall, but I think it is a waste of time.

Again, I don't believe there is anything you can do with this library that you can't do without this library. You are trying to use a .NET wrapper around the unmanaged library UI Accessibility 2. I would highly recommend that you use UIA directly.

I am not sure what version of UIA the existing scripts are for. It is outside my experience and the scope of this topic.
User avatar
Shield
Posts: 9
Joined: 07 Dec 2021, 17:55

Re: .NET Framework Interop (CLR, C#, VB)

24 Oct 2022, 04:30

Thank you for the explanation, @lexikos. :)
lexikos wrote:
24 Oct 2022, 02:32
Again, I don't believe there is anything you can do with this library that you can't do without this library. You are trying to use a .NET wrapper around the unmanaged library UI Accessibility 2. I would highly recommend that you use UIA directly.

I am not sure what version of UIA the existing scripts are for.
Unfortunately the quote that @malcev citied was mixing up the different versions. The UIA2 lib is in fact the managed one, not vice versa. I had been using (the unmanaged) UIA directly, but it can't handle that control the way the managed one does. This image outlines the differences:

Image

... and that is why I'm confident that CLR.ahk is the key to success. Nonetheless I don't get why my code doesn't find the FromPoint method.
malcev
Posts: 1769
Joined: 12 Aug 2014, 12:37

Re: .NET Framework Interop (CLR, C#, VB)

24 Oct 2022, 09:08

Why You just cannot compile c# code with Your class and run Your method from memory?
malcev
Posts: 1769
Joined: 12 Aug 2014, 12:37

Re: .NET Framework Interop (CLR, C#, VB)

27 Oct 2022, 01:34

WinScp has comobject, You dont need to run it with с#.
mINXZKA
Posts: 13
Joined: 16 Mar 2016, 03:47

Re: .NET Framework Interop (CLR, C#, VB)

27 Oct 2022, 01:54

Admin rights are required for DLL registration. I actually wanted to avoid this. (https://github.com/lipkau/WinSCP.ahk)

Code: Select all

%WINDIR%\Microsoft.NET\Framework\v4.0.30319\RegAsm.exe WinSCPnet.dll /codebase WinSCPnet.dll /tlb
RegAsm : error RA0000

And with the SFTP command from Windows I cannot send a password / key-fingerprint automatically.
(https://github.com/PowerShell/Win32-OpenSSH/wiki/sftp.exe-examples)
malcev
Posts: 1769
Joined: 12 Aug 2014, 12:37

Re: .NET Framework Interop (CLR, C#, VB)

27 Oct 2022, 02:25

You have ready-made examples of using it with c# and powershell.
If You cannot run with c#, run with powershell.
malcev
Posts: 1769
Joined: 12 Aug 2014, 12:37

Re: .NET Framework Interop (CLR, C#, VB)

28 Oct 2022, 05:23

As for FlaUI.UIA2.dll, for me works like this (I dont know c# at all)

Code: Select all

cSharp =
(
   using FlaUI.Core.AutomationElements.Infrastructure;
   using FlaUI.UIA2;
   using FlaUI.Core.Shapes;
   using FlaUI.Core.Input;

   class foo
   {
      public string Test()
      {
         var automation = new FlaUI.UIA2.UIA2Automation();
         var treeWalker = new UIA2TreeWalkerFactory(automation).GetContentViewWalker();
         Point pt = Mouse.Position;
         AutomationElement e = automation.FromPoint(pt);
         double x = e.Properties.BoundingRectangle.Value.X + e.Properties.BoundingRectangle.Value.Width;
         while (pt.X >= x)
         {
            e = treeWalker.GetNextSibling(e);
            x = e.Properties.BoundingRectangle.Value.X + e.Properties.BoundingRectangle.Value.Width;
         }
         automation.Dispose();
         return e.Properties.Name;
      }
   }
)
asm := CLR_CompileC#(cSharp, "C:\Program Files\AutoHotkey\FlaUI.Core.dll | C:\Program Files\AutoHotkey\FlaUI.UIA2.dll")
obj := CLR_CreateObject(asm, "foo")
return

f11:: msgbox % obj.Test()
malcev
Posts: 1769
Joined: 12 Aug 2014, 12:37

Re: .NET Framework Interop (CLR, C#, VB)

30 Oct 2022, 05:47

The same without 3rd party libraries

Code: Select all

cSharp =
(
   using System.Windows.Forms;
   using System.Windows.Automation;

   class foo
   {
      public string Test()
      {
         System.Windows.Point point = new System.Windows.Point(Cursor.Position.X, Cursor.Position.Y);
         AutomationElement element = AutomationElement.FromPoint(point);
         TreeWalker treeWalker = TreeWalker.ContentViewWalker;
         double x = element.Current.BoundingRectangle.X + element.Current.BoundingRectangle.Width;
         while (point.X >= x)
         {
            element = treeWalker.GetNextSibling(element);
            x = element.Current.BoundingRectangle.X + element.Current.BoundingRectangle.Width;
         }
         return element.Current.Name;
      }
   }
)
asm := CLR_CompileC#(cSharp, "System.Drawing.dll | System.Windows.Forms.dll | C:\Windows\Microsoft.NET\assembly\GAC_MSIL\UIAutomationClient\v4.0_4.0.0.0__31bf3856ad364e35\UIAutomationClient.dll | C:\Windows\Microsoft.NET\assembly\GAC_MSIL\WindowsBase\v4.0_4.0.0.0__31bf3856ad364e35\WindowsBase.dll")
obj := CLR_CreateObject(asm, "foo")
return

f11:: msgbox % obj.Test()
User avatar
Shield
Posts: 9
Joined: 07 Dec 2021, 17:55

Re: .NET Framework Interop (CLR, C#, VB)

01 Nov 2022, 16:40

@malcev:
Your example w/o 3rd party libraries always returns an empty MsgBox here, but the FlaUI version does work! :thumbup: Thank you for this, no doubt that this is something I can build upon. And an even bigger thanks for pointing me to http://automationspy.freecluster.eu/. I spend the last few days dealing with that UIDeskAutomation.dll (+ getting along with c#) , and was able to create this working solution:

Code: Select all

; Using CLR.ahk v1.2

; Requirements:
; UIDeskAutomation.dll
; A running instance of file manager xplorer2 lite, for testing purposes (file pane is a custom ListView control)

cSharp =
	(
			using System;
			using System.Windows.Forms;
			using UIDeskAutomationLib;

			class foo {

				public string[,] Readout(int ProcessID) {

				var engine = new Engine();
				UIDA_DataGrid TheGrid = null;
				int NumberOfColumns = 0;

				// Find the custom ListView control
				TheGrid = engine.GetTopLevelByProcId(ProcessID, "*", "ATL:ExplorerFrame").PaneAt("", 2).PaneAt("", 2).Pane("").DataGrid("Left pane settings");
				NumberOfColumns = TheGrid.ColumnCount;

				// Copy the content into a UIDA_DataItem[] as it provides significantly faster access
				UIDA_DataItem[] AllRows = TheGrid.Rows;

				// Creating the output Array
				int NumberOfRows = TheGrid.RowCount;
				string[,] ArrayOfListView = new string[NumberOfRows + 1, NumberOfColumns + 1];

				// Filling the Array with the grids contents - including the index of the selected row!
				for (int RowX = 0; RowX < NumberOfRows ; RowX++)
					{
						if (AllRows[RowX].IsSelected)
							ArrayOfListView[0,0] = (RowX + 1).ToString();

						for (int ColumnX = 0; ColumnX < NumberOfColumns; ColumnX++)
							{
								ArrayOfListView[RowX + 1,ColumnX + 1] = AllRows[RowX][ColumnX];
							}
					}

				return ArrayOfListView;
			}
		}
	)

; -----

Process, Exist, xplorer2_lite.exe
ProcessID := ErrorLevel
If not ProcessID
	{	MsgBox, xplorer2 lite not found.`n`nExiting.
		ExitApp
	}

asm := CLR_CompileC#(cSharp, "System.dll | " DotNetPath "System.Windows.Forms.dll | UIDeskAutomation.dll")
obj := CLR_CreateObject(asm, "foo")

ArrayOfListView := obj.Readout(ProcessID)

If (not ArrayOfListView.MaxIndex() OR not ArrayOfListView[1,1])
	MsgBox, No content has been found.
else
	{	If (ArrayOfListView[0,0])
			MsgBox, % "Selected row:`n`n" ArrayOfListView[ArrayOfListView[0,0],1]
		else
			MsgBox, There is no row selected.

		msgbox % ArrayOfListView[1,1] "`n" ArrayOfListView[1,2] "`n" ArrayOfListView[1,3] "`n" ArrayOfListView[1,4] "`n" ArrayOfListView[1,5] 
		msgbox % ArrayOfListView[2,1] "`n" ArrayOfListView[2,2] "`n" ArrayOfListView[2,3] "`n" ArrayOfListView[2,4] "`n" ArrayOfListView[2,5] 
	}

ExitApp

Esc::
ExitApp


; ==== CLR.ahk v1.2 by Lexikos  ====


CLR_LoadLibrary(AssemblyName, AppDomain=0)
{
	if !AppDomain
		AppDomain := CLR_GetDefaultDomain()
	e := ComObjError(0)
	Loop 1 {
		if assembly := AppDomain.Load_2(AssemblyName)
			break
		static null := ComObject(13,0)
		args := ComObjArray(0xC, 1),  args[0] := AssemblyName
		typeofAssembly := AppDomain.GetType().Assembly.GetType()
		if assembly := typeofAssembly.InvokeMember_3("LoadWithPartialName", 0x158, null, null, args)
			break
		if assembly := typeofAssembly.InvokeMember_3("LoadFrom", 0x158, null, null, args)
			break
	}
	ComObjError(e)
	return assembly
}

CLR_CreateObject(Assembly, TypeName, Args*)
{
	if !(argCount := Args.MaxIndex())
		return Assembly.CreateInstance_2(TypeName, true)
	
	vargs := ComObjArray(0xC, argCount)
	Loop % argCount
		vargs[A_Index-1] := Args[A_Index]
	
	static Array_Empty := ComObjArray(0xC,0), null := ComObject(13,0)
	
	return Assembly.CreateInstance_3(TypeName, true, 0, null, vargs, null, Array_Empty)
}

CLR_CompileC#(Code, References="", AppDomain=0, FileName="", CompilerOptions="")
{
	return CLR_CompileAssembly(Code, References, "System", "Microsoft.CSharp.CSharpCodeProvider", AppDomain, FileName, CompilerOptions)
}

CLR_CompileVB(Code, References="", AppDomain=0, FileName="", CompilerOptions="")
{
	return CLR_CompileAssembly(Code, References, "System", "Microsoft.VisualBasic.VBCodeProvider", AppDomain, FileName, CompilerOptions)
}

CLR_StartDomain(ByRef AppDomain, BaseDirectory="")
{
	static null := ComObject(13,0)
	args := ComObjArray(0xC, 5), args[0] := "", args[2] := BaseDirectory, args[4] := ComObject(0xB,false)
	AppDomain := CLR_GetDefaultDomain().GetType().InvokeMember_3("CreateDomain", 0x158, null, null, args)
	return A_LastError >= 0
}

CLR_StopDomain(ByRef AppDomain)
{	; ICorRuntimeHost::UnloadDomain
	DllCall("SetLastError", "uint", hr := DllCall(NumGet(NumGet(0+RtHst:=CLR_Start())+20*A_PtrSize), "ptr", RtHst, "ptr", ComObjValue(AppDomain))), AppDomain := ""
	return hr >= 0
}

; NOTE: IT IS NOT NECESSARY TO CALL THIS FUNCTION unless you need to load a specific version.
CLR_Start(Version="") ; returns ICorRuntimeHost*
{
	static RtHst := 0
	; The simple method gives no control over versioning, and seems to load .NET v2 even when v4 is present:
	; return RtHst ? RtHst : (RtHst:=COM_CreateObject("CLRMetaData.CorRuntimeHost","{CB2F6722-AB3A-11D2-9C40-00C04FA30A3E}"), DllCall(NumGet(NumGet(RtHst+0)+40),"uint",RtHst))
	if RtHst
		return RtHst
	EnvGet SystemRoot, SystemRoot
	if Version =
		Loop % SystemRoot "\Microsoft.NET\Framework" (A_PtrSize=8?"64":"") "\*", 2
			if (FileExist(A_LoopFileFullPath "\mscorlib.dll") && A_LoopFileName > Version)
				Version := A_LoopFileName
	if DllCall("mscoree\CorBindToRuntimeEx", "wstr", Version, "ptr", 0, "uint", 0
	, "ptr", CLR_GUID(CLSID_CorRuntimeHost, "{CB2F6723-AB3A-11D2-9C40-00C04FA30A3E}")
	, "ptr", CLR_GUID(IID_ICorRuntimeHost,  "{CB2F6722-AB3A-11D2-9C40-00C04FA30A3E}")
	, "ptr*", RtHst) >= 0
		DllCall(NumGet(NumGet(RtHst+0)+10*A_PtrSize), "ptr", RtHst) ; Start
	return RtHst
}

;
; INTERNAL FUNCTIONS
;

CLR_GetDefaultDomain()
{
	static defaultDomain := 0
	if !defaultDomain
	{	; ICorRuntimeHost::GetDefaultDomain
		if DllCall(NumGet(NumGet(0+RtHst:=CLR_Start())+13*A_PtrSize), "ptr", RtHst, "ptr*", p:=0) >= 0
			defaultDomain := ComObject(p), ObjRelease(p)
	}
	return defaultDomain
}

CLR_CompileAssembly(Code, References, ProviderAssembly, ProviderType, AppDomain=0, FileName="", CompilerOptions="")
{
	if !AppDomain
		AppDomain := CLR_GetDefaultDomain()
	
	if !(asmProvider := CLR_LoadLibrary(ProviderAssembly, AppDomain))
	|| !(codeProvider := asmProvider.CreateInstance(ProviderType))
	|| !(codeCompiler := codeProvider.CreateCompiler())
		return 0

	if !(asmSystem := (ProviderAssembly="System") ? asmProvider : CLR_LoadLibrary("System", AppDomain))
		return 0
	
	; Convert | delimited list of references into an array.
	StringSplit, Refs, References, |, %A_Space%%A_Tab%
	aRefs := ComObjArray(8, Refs0)
	Loop % Refs0
		aRefs[A_Index-1] := Refs%A_Index%
	
	; Set parameters for compiler.
	prms := CLR_CreateObject(asmSystem, "System.CodeDom.Compiler.CompilerParameters", aRefs)
	, prms.OutputAssembly          := FileName
	, prms.GenerateInMemory        := FileName=""
	, prms.GenerateExecutable      := SubStr(FileName,-3)=".exe"
	, prms.CompilerOptions         := CompilerOptions
	, prms.IncludeDebugInformation := true
	
	; Compile!
	compilerRes := codeCompiler.CompileAssemblyFromSource(prms, Code)
	
	if error_count := (errors := compilerRes.Errors).Count
	{
		error_text := ""
		Loop % error_count
			error_text .= ((e := errors.Item[A_Index-1]).IsWarning ? "Warning " : "Error ") . e.ErrorNumber " on line " e.Line ": " e.ErrorText "`n`n"
		MsgBox, 16, Compilation Failed, %error_text%
		return 0
	}
	; Success. Return Assembly object or path.
	return compilerRes[FileName="" ? "CompiledAssembly" : "PathToAssembly"]
}

CLR_GUID(ByRef GUID, sGUID)
{
	VarSetCapacity(GUID, 16, 0)
	return DllCall("ole32\CLSIDFromString", "wstr", sGUID, "ptr", &GUID) >= 0 ? &GUID : ""
}
I am pretty happy with this, the only downside is speed. Especially the two nested loops which copy the content of the UIDA_DataItem[] into the array are somewhat time-consuming, the context menu which is going to follow may take several seconds to appear. I don't know a faster way yet, but if I would be able to return the UIDA_DataItem to AutoHotkey, I could do the full readout after the menu. It's possible that I won't even need an array that way. So I tried this one:

Code: Select all

uida := CLR_LoadLibrary("UIDeskAutomation.dll")
DataItemObject := CLR_CreateObject(uida, "UIDA_DataItem")
... and several variations, but none of them is working. I don't even get an error, IsObject() just always return zero. :?
sp00ky
Posts: 1
Joined: 05 Jan 2023, 10:22

Re: .NET Framework Interop (CLR, C#, VB)

05 Jan 2023, 10:34

Hi guys,

I'm new in AutoHotKey, previously I used AutoIT.

As I need to use functions in a DLL (.NET) I found this language few days ago, and tried to program my first script and use my DLL.

My code is below:

Code: Select all

#SingleInstance force
#include CLR.ahk


asm := CLR_LoadLibrary("SLLDRemoteControl.dll")
jw := CLR_CreateObject(asm, "SLLDRemoteControl.SLLD_Functions")

;_connect := jw.GetVersion()
Sleep 3000

IP := 0x7F000001
PORT := 5000
_connect := jw.OpenConnection(IP, PORT)


msgbox % "return:" _connect
    ExitApp
And the dll has the function "OpenConnection" as below:

Code: Select all

public int OpenConnection(ref byte[] ipAdr, int Port)
    {
      if (this.once)
        return 0;
      try
      {
        this.InitializeConnection(ipAdr, Port);
        byte[] data = TelegramBuilder.MakeCommand(1, (object) "1990");
        if (data == null)
          return 0;
        SLAnswer ca;
        if (this.SendAndReceive(out ca, data) != 1)
          ca.cmd = 0;
        if (ca.cmd == 1)
        {
          int retVal = this.retVal;
          this.GetVersion();
          return retVal;
        }
        if (ca.cmd == 0 || ca.cmd == 1)
          return 0;
        this.m_transmission_error |= 2;
        return 0;
      }
      catch
      {
        this.th.m_go = false;
        return -1;
      }
    }
So I need to open the Class "SLLDRemoteControl.SLLD_Functions", call the function "OpenConnection" and use arguments ipAdr and Port.
The first one is made of byte, as the Doc precises:
Argument(s): byte[] ipAdr is the IP address (ipv4) of
the laserDESK server, most significant byte first (e.g.
127.0.0.1 corresponds to byte[0] = 127, byte[1] = 0,
byte[2] = 0, byte[3] = 1).
Unfortunately, my script doesn't work and I obtain Error: 0x80070057 - Incorrect parameter.

Please, could you highlight my error?

Thank you

Return to “Scripts and Functions (v1)”

Who is online

Users browsing this forum: Google [Bot] and 127 guests