rich text edit control in AHK V2?

Get help with using AutoHotkey (v2 or newer) and its commands and hotkeys
Spitzi
Posts: 313
Joined: 24 Feb 2022, 03:45

rich text edit control in AHK V2?

21 Sep 2023, 11:25

Hi there.

I would like to be able to formal text in an edit control - e.g. make some lines bold, include a hyperlink that can be clicked like a button... the like.

Is there anything to support that in AHK V2? Or workarounds that I could use?

Thanks Spitzi
emp00
Posts: 156
Joined: 15 Apr 2023, 12:02

Re: rich text edit control in AHK V2?

21 Sep 2023, 14:46

@Spitzi - check out Clipster posted 2 days ago by The Automator:

Sep 19, 2023 Sadly sending keystrokes is becoming unreliable. We wrote a function that pastes the text but much more! It also will: Navigate to a folder, Paste HTML or Paste Images. Check it out in this video, you can get the script here https://the-Automator.com/Clipster
teadrinker
Posts: 4335
Joined: 29 Mar 2015, 09:41
Contact:

Re: rich text edit control in AHK V2?

21 Sep 2023, 22:29

RichEdit:

Code: Select all

#Requires AutoHotkey v2.0
#DllLoad 'Msftedit.dll'

styles := (ES_SAVESEL := 0x00008000) | (ES_NOHIDESEL := 0x00000100) | (ES_MULTILINE := 0x00000004)
EN_LINK := 0x70B, ENM_LINK := 0x04000000, tomNullCaret := 2, WM_LBUTTONDOWN := 0x0201
EM_SETREADONLY := 0x00CF, EM_SETEVENTMASK := 0x0445, EM_SETRECT := 0x00B3

imagePath := 'pic.png'
if !FileExist(imagePath) {
    Download 'https://i.imgur.com/26lSSUi.png', imagePath
}

wnd := Gui(, 'RichEdit Test')
wnd.MarginX := wnd.MarginY := 15
re := wnd.AddCustom('ClassRICHEDIT50W x15 y15 w395 h250 ' . styles)

SendMessage EM_SETEVENTMASK,, ENM_LINK, re
NumPut('Int', 14, 'Int', 18, 'Int', 373, 'Int', 225, RECT := Buffer(16))
SendMessage EM_SETRECT,, RECT, re

TextDocument := GetDispatch(re.hwnd)
TextDocument.CaretType := tomNullCaret

rng := CreateRange(0, 0, 'Calibri', 28, 0x38D3E2)
rng.Text := 'This is a RichEdit control'

rng := CreateRange(rng.End, rng.End,, 5)
rng.Text := '`n`n'

rng := CreateRange(rng.End, rng.End, 'Consolas', 18)
rng.Text := 'AutoHotkey'
rng.Url := '"https://www.autohotkey.com/boards/viewtopic.php?p=540158#p540158"'

rng := CreateRange(rng.End, rng.End, 'Consolas', 18)
rng.Text := ' 🠔 this is a link'

rng := CreateRange(rng.End, rng.End,, 12)
sp := '                 '
Loop 9 * A_ScreenDPI // 100 {
    sp .= A_Space
}
rng.Text := '`n`n`n' . sp

InsertPicture(imagePath, CreateRange(rng.End, rng.End), 2000, 2000) ; HIMETRIC := 0.01 mm

SendMessage EM_SETREADONLY, true,, re
wnd.Show()

re.OnNotify(EN_LINK, (e, lParam) => (
    NumGet(lParam, A_PtrSize * 3, 'UInt') = WM_LBUTTONDOWN && (
        start := NumGet(lParam, A_PtrSize * 5 + 4, 'UInt'),
        end   := NumGet(lParam, A_PtrSize * 5 + 8, 'UInt'),
        Run(TextDocument.Range(start, end).Text)
    )
))

CreateRange(start, end, fontName := 'Verdana', fontSize := 12, fontColor?) {
    rng := TextDocument.Range(start, end)
    rng.Font.Name := fontName
    rng.Font.Size := fontSize
    IsSet(fontColor) && rng.Font.ForeColor := fontColor
    return rng
}

InsertPicture(imagePath, rng, w, h) {
    static VT_UNKNOWN := 13, TA_BASELINE := 0x18
    buf := FileRead(imagePath, 'RAW')
    pIStream := DllCall('Shlwapi\SHCreateMemStream', 'Ptr', buf, 'UInt', buf.size, 'Ptr')
    rng.InsertImage(w, h, 1, TA_BASELINE, '', ComValue(VT_UNKNOWN, pIStream))
}

GetDispatch(hEdit) {
    static EM_GETOLEINTERFACE := 0x43C, VT_DISPATCH := 9, VT_UNKNOWN := 13, F_OWNVALUE := 1
         , IID_ITextDocument2 := '{01C25500-4268-11D1-883A-3C8B00C10000}'
         
    DllCall('SendMessage', 'Ptr', hEdit, 'UInt', EM_GETOLEINTERFACE,
                           'Ptr', 0, 'PtrP', IRichEditOle := ComValue(VT_UNKNOWN, 0))
    ITextDocument := ComObjQuery(IRichEditOle, IID_ITextDocument2)
    return ComValue(VT_DISPATCH, ITextDocument, F_OWNVALUE)
}
Last edited by teadrinker on 21 Nov 2023, 00:37, edited 2 times in total.
User avatar
kczx3
Posts: 1643
Joined: 06 Oct 2015, 21:39

Re: rich text edit control in AHK V2?

22 Sep 2023, 08:28

@teadrinker somewhat of an aside but how does that work by setting properties on the text range? The ITextRange2 interface shows all methods for getters and setters of properties.
teadrinker
Posts: 4335
Joined: 29 Mar 2015, 09:41
Contact:

Re: rich text edit control in AHK V2?

22 Sep 2023, 11:29

I'm not quite sure what exactly you are asking. Why I use rng.Text but not rng.SetText?
Anyway, you can see all the property and method names like this:

Code: Select all

#DllLoad 'Msftedit.dll'

testGui := Gui()
re := testGui.AddCustom('ClassRICHEDIT50W')
TextDocument := GetDispatch(re.hwnd)
rng := TextDocument.Range(0, 0)

infoMap := EnumComMembers(rng)

wnd := Gui('Resize Owner', 'ITextRange2 Class Members')
wnd.SetFont('s11', 'Calibri')
wnd.OnEvent('Size', (g, mm, w, h) => lv.Move(,, w, h))
wnd.MarginX := wnd.MarginY := 0
headers := ['ID', 'Name', 'Description', 'Kind', 'Arg count', 'Opt args']
lv := wnd.AddListView('Grid R' . (infoMap.Count > 30 ? 31 : infoMap.Count + 1), headers)
for id, info in infoMap {
    lv.Add('Vis', id, info.name, info.desc, info.kind, info.argNum, info.optArgs)
}
Loop headers.Length {
    lv.ModifyCol(A_Index, 'AutoHdr' . (A_Index > 4 ? ' Center' : ''))
}
lv.OnEvent('ContextMenu', (lv, item, *) => (ToolTip('Copied: ' . A_Clipboard := lv.GetText(item, 2)), Sleep(800), ToolTip()))
wnd.Show('w900')

EnumComMembers(IDispatch) {
    static VT_DISPATCH := 9, VT_UNKNOWN := 13, MEMBERID_NIL := -1
    if ComObjType(IDispatch) != VT_DISPATCH {
        return MsgBox('IDispatch type only supported')
    }
    pIDispatch := ComObjValue(IDispatch)
    ComCall(GetTypeInfoCount := 3, pIDispatch, 'UIntP', &hasInfo := 0)
    if !hasInfo {
        return MsgBox('ITypeInfo Interface not supported')
    }
    ComCall(GetTypeInfo := 4, pIDispatch, 'UInt', 0, 'UInt', 0, 'PtrP', ITypeInfo := ComValue(VT_UNKNOWN, 0))
    name := 'IDispatch'
    Loop {
        ComCall(GetTypeAttr := 3, ITypeInfo, 'PtrP', &pTypeAttr := 0)
        cFuncs     := NumGet(pTypeAttr, 40 + A_PtrSize, 'Short')
        cImplTypes := NumGet(pTypeAttr, 44 + A_PtrSize, 'Short')
        ComCall(ReleaseTypeAttr := 19, ITypeInfo, 'Ptr', pTypeAttr)
        
        if cImplTypes {
            ComCall(GetRefTypeOfImplType := 8, ITypeInfo, 'Int', 0, 'PtrP', &pRefType := 0)
            ComCall(GetRefTypeInfo := 14, ITypeInfo, 'Ptr', pRefType, 'PtrP', ITypeInfo2 := ComValue(VT_UNKNOWN, 0))
            ComCall(GetDocumentation := 12, ITypeInfo2, 'Int', MEMBERID_NIL, 'PtrP', &pName := 0, 'Ptr', 0, 'Ptr', 0, 'Ptr', 0)
            name := StrGet(pName)
            DllCall('OleAut32\SysFreeString', 'Ptr', pName)
            if name != 'IDispatch' {
                ITypeInfo := ITypeInfo2
            }
        }
    } until name == 'IDispatch'
    info := Map()
    Loop cFuncs {
        ComCall(GetFuncDesc := 5, ITypeInfo, 'UInt', A_Index - 1, 'PtrP', &pFuncDesc := 0)
        id      := NumGet(pFuncDesc,                  0, 'Short')
        invkind := NumGet(pFuncDesc,  4 + A_PtrSize * 3, 'Short')
        argNum  := NumGet(pFuncDesc, 12 + A_PtrSize * 3, 'Short')
        optArgs := NumGet(pFuncDesc, 14 + A_PtrSize * 3, 'Short')
        ComCall(ReleaseFuncDesc := 20, ITypeInfo, 'Ptr', pFuncDesc)
        try ComCall(GetDocumentation := 12, ITypeInfo, 'Int', id, 'PtrP', &pName := 0, 'PtrP', &pDesc := 0, 'Ptr', 0, 'Ptr', 0)
        catch {
            continue
        }
        name := StrGet(pName), desc := ''
        (pDesc && desc := StrGet(pDesc))
        DllCall('OleAut32\SysFreeString', 'Ptr', pName), DllCall('OleAut32\SysFreeString', 'Ptr', pDesc)
        kind := ['method', 'get',, 'put',,,, 'putref'][invkind]
        if info.Has(id) && info[id].kind != 'method' && !(info[id].kind ~= '(^|,\s)' . kind . '(,|$)') {
            info[id].kind .= ', ' . kind, info[id].argNum := argNum, info[id].optArgs := optArgs
        } else {
            info[id] := {name: name, desc: desc, kind: kind, argNum: argNum, optArgs: optArgs}
        }
    }
    return info
}

GetDispatch(hEdit) {
    static EM_GETOLEINTERFACE := 0x43C, VT_DISPATCH := 9, VT_UNKNOWN := 13, F_OWNVALUE := 1
         , IID_ITextDocument2 := '{01C25500-4268-11D1-883A-3C8B00C10000}'
         
    DllCall('SendMessage', 'Ptr', hEdit, 'UInt', EM_GETOLEINTERFACE,
                           'Ptr', 0, 'PtrP', IRichEditOle := ComValue(VT_UNKNOWN, 0))
    ITextDocument := ComObjQuery(IRichEditOle, IID_ITextDocument2)
    Return ComValue(VT_DISPATCH, ITextDocument, F_OWNVALUE)
}
teadrinker
Posts: 4335
Joined: 29 Mar 2015, 09:41
Contact:

Re: rich text edit control in AHK V2?

22 Sep 2023, 20:23

Perhaps for scripting languages getters and setters are overridden in properties, I don't have enough knowledge to explain what might trigger such a change. :)
teadrinker
Posts: 4335
Joined: 29 Mar 2015, 09:41
Contact:

Re: rich text edit control in AHK V2?

22 Sep 2023, 20:33

An example you are probably familiar with: IAccessible::put_accValue and IAccessible::get_accValue in AHK is simply AccObj.accValue().
User avatar
kczx3
Posts: 1643
Joined: 06 Oct 2015, 21:39

Re: rich text edit control in AHK V2?

22 Sep 2023, 21:07

I think it’s because those interfaces all derive from iDispatch and AHK does some things in the background and tries to call or set the property. I’m sure I’m using the wrong terminology but I’ve seen Lexikos discuss it with respect to JavaScript stuff in IE
User avatar
thqby
Posts: 408
Joined: 16 Apr 2021, 11:18
Contact:

Re: rich text edit control in AHK V2?

22 Sep 2023, 21:11

IAccessible::get_accValue in AHK is simply AccObj.accValue[].

As far as I know, the members and calling methods of interface IDispatch are defined in the idl file.
By default, the property xx decorated with propget in idl file will generate a declaration of get_xx in the header file after compilation.
Maybe this behavior can be changed, for example, generating method names with Getxx style.
Spitzi
Posts: 313
Joined: 24 Feb 2022, 03:45

Re: rich text edit control in AHK V2?

23 Sep 2023, 09:07

@teadrinker , this is quite impressive! Thank you.

If I wanted the link to trigger an action in AHK, would that be possible, too?
And how about loading a .rtf-File into the edit element?
teadrinker
Posts: 4335
Joined: 29 Mar 2015, 09:41
Contact:

Re: rich text edit control in AHK V2?

23 Sep 2023, 11:41

Spitzi wrote: If I wanted the link to trigger an action in AHK, would that be possible
Of course, I have separated the handling of clicking on a link into the OnLinkClick() function:

Code: Select all

#Requires AutoHotkey v2.0

#DllLoad 'Msftedit.dll'

styles := (ES_SAVESEL := 0x00008000) | (ES_NOHIDESEL := 0x00000100) | (ES_MULTILINE := 0x00000004)
margins := (EC_LEFTMARGIN := 1) | (EC_RIGHTMARGIN := 2)
WM_LBUTTONDOWN := 0x0201, EM_SETREADONLY := 0x00CF, EM_SETEVENTMASK := 0x0445, EM_SETMARGINS := 0x00D3
EN_LINK := 0x70B, ENM_LINK := 0x04000000, tomNullCaret := 2

imagePath := 'pic.png'
if !FileExist(imagePath) {
    Download 'https://i.imgur.com/26lSSUi.png', imagePath
}

wnd := Gui(, 'RichEdit Test')
wnd.MarginX := wnd.MarginY := 15
re := wnd.AddCustom('ClassRICHEDIT50W x15 y15 w395 h250 ' . styles)

SendMessage EM_SETEVENTMASK,, ENM_LINK, re
SendMessage EM_SETMARGINS, margins, 12 | 12 << 16, re

TextDocument := GetDispatch(re.hwnd)
TextDocument.CaretType := tomNullCaret

rng := CreateRange(0, 0,, 5)
rng.Text := '`n'

rng := CreateRange(rng.End, rng.End, 'Calibri', 28, 0x38D3E2)
rng.Text := 'This is a RichEdit control'

rng := CreateRange(rng.End, rng.End,, 5)
rng.Text := '`n`n'

rng := CreateRange(rng.End, rng.End, 'Consolas', 18)
rng.Text := 'AutoHotkey'
rng.Url := '"https://www.autohotkey.com/boards/viewtopic.php?p=540158#p540158"'

rng := CreateRange(rng.End, rng.End, 'Consolas', 18)
rng.Text := ' 🠔 this is a link'

rng := CreateRange(rng.End, rng.End,, 12)
sp := '                 '
Loop 9 * A_ScreenDPI // 100 {
    sp .= A_Space
}
rng.Text := '`n`n`n' . sp

InsertPicture(imagePath, CreateRange(rng.End, rng.End), 2000, 2000) ; HIMETRIC := 0.01 mm
SendMessage EM_SETREADONLY, true,, re
wnd.Show()

OnLinkClick(link) => MsgBox(link)

re.OnNotify(EN_LINK, (e, lParam) => (
    NumGet(lParam, A_PtrSize * 3, 'UInt') = WM_LBUTTONDOWN && (
        start := NumGet(lParam, A_PtrSize * 5 + 4, 'UInt'),
        end   := NumGet(lParam, A_PtrSize * 5 + 8, 'UInt'),
        OnLinkClick(TextDocument.Range(start, end).Text)
    )
))

CreateRange(start, end, fontName := 'Verdana', fontSize := 12, fontColor?) {
    rng := TextDocument.Range(start, end)
    rng.Font.Name := fontName
    rng.Font.Size := fontSize
    IsSet(fontColor) && rng.Font.ForeColor := fontColor
    return rng
}

InsertPicture(imagePath, rng, w, h) {
    buf := FileRead(imagePath, 'RAW')
    BSTR := DllCall('OleAut32\SysAllocString', 'Str', 'pic')
    pIStream := DllCall('Shlwapi\SHCreateMemStream', 'Ptr', buf, 'UInt', buf.size, 'Ptr')
    ComCall(InsertImage := 107, ComObjValue(rng), 'UInt', w, 'UInt', h, 'UInt', 1,
                                'UInt', TA_BASELINE := 0x18, 'Ptr', BSTR, 'Ptr', pIStream)
    ObjRelease(pIStream)
}

GetDispatch(hEdit) {
    static EM_GETOLEINTERFACE := 0x43C, VT_DISPATCH := 9, VT_UNKNOWN := 13, F_OWNVALUE := 1
         , IID_ITextDocument2 := '{01C25500-4268-11D1-883A-3C8B00C10000}'
         
    DllCall('SendMessage', 'Ptr', hEdit, 'UInt', EM_GETOLEINTERFACE,
                           'Ptr', 0, 'PtrP', IRichEditOle := ComValue(VT_UNKNOWN, 0))
    ITextDocument := ComObjQuery(IRichEditOle, IID_ITextDocument2)
    Return ComValue(VT_DISPATCH, ITextDocument, F_OWNVALUE)
}
Spitzi wrote: And how about loading a .rtf-File into the edit element?
I haven't tested it yet, but I don't think there should be a problem with it. There is the ITextDocument::Open method.

Return to “Ask for Help (v2)”

Who is online

Users browsing this forum: Descolada, hisrRB57, Insaid, ManuelesAdrian and 30 guests