Skip to content

Add Image to AutoHotkey GUI

Edison Hua edited this page Jun 30, 2024 · 56 revisions

Default AutoHotkey example using Gui.AddPicture:

1

Animations with ImagePut: [code + image]

Note: Supports WEBP and HEIC. Does not support animated AVIF.

Animation

Wow! What a difference.

Tooltip

Note: If you'd like to enable the ability to drag the image and tooltips, just remove the WS_DISABLED := 0x08000000 window style in the code below.

Example #1 - Using ImageShow Directly (scroll down for a better version)

This example shows the inner workings of how ImagePut can take over an AutoHotkey GUI control.

#Requires AutoHotkey v2.0
#include ImagePut.ahk

image := FileSelect(,, "Select an image:", "Images (*.bmp; *.dib; *.rle; *.jpg; *.jpeg; *.jpe; *.jfif; *.gif; *.emf; *.wmf; *.tif; *.tiff; *.png; *.ico; *.heic; *.hif; *.webp; *avif; *avifs)")

; Make the GUI resizable and not affected by DPI scaling.
app := Gui("-DPIScale +Resize")

; Sets the image filepath as the window title.
app.Title := image

; Create a dummy text control to repurpose for ImagePut's functionality.
display := app.Add("Text", "xm+0")

; Must resize the viewable area of the control.
display.move(,,ImageWidth(image), ImageHeight(image))

; Use ImagePut to create a child window, and set the parent as the text control.
image_hwnd := ImageShow(image,, [0, 0], 0x40000000 | 0x10000000 | 0x8000000,, display.hwnd)

; Show the image
app.Show("xCenter y0 AutoSize")

Looking closely at ImageShow we see that:

image_hwnd := ImageShow(
    image,                   ; The input image of any type
    "",                      ; Window Title can never be seen
    [0, 0],                  ; Sets the [x, y, w, h, r] coordinate to top-left. 
    0x40000000 | 0x10000000, ; WS_CHILD and WS_VISIBLE (Note: Add WS_DISABLED := 0x8000000)
    0x80000,                 ; WS_EX_LAYERED
    display.hwnd,            ; Sets the GuiControl as the parent
    True)                    ; Start Animation?

First, the x, y coordinates must be set to (0, 0) to position at the top-left of the GuiControl. Forgetting to do this would default to the middle of the screen, which would lead to a large offset like (960, 540) on a 1920 × 1080 monitor.

An additional desired width or height can be set in w and h. You may choose to omit either the width or height to preserve the existing aspect ratio. E.g. [0,0,100,100] or [0,0,"auto",100] (Specifying auto is optional.)

If a r parameter is specified, the x and y coordinates will be offset relative to that window. For example, [30, 30, 100, 100, "ahk_class notepad"] would show the window 30 pixels inside the notepad window.

The window style WS_CHILD signals that this is a child window and that there will be a parent parameter passed through. Attempting to pass a parent parameter without setting WS_CHILD is probably an error. See Raymond Chen's blog post for an explanation on parent vs owner.

Setting WS_DISABLED := 0x08000000 will disable Left Click to drag and Middle Click for tooltip and Right Click to delete.

The WS_EX_LAYERED value must be set or ImagePut will forcibly set this value for you. Otherwise the call to UpdateLayeredWindow wouldn't work!

Now we have all the pieces in place for making a better Gui.AddPicture() function.

AddImageToControl(image, GuiControl) {
    return ImageShow(image,, [0, 0], 0x40000000 | 0x10000000,, GuiControl.hwnd, True)
}

Example #2 - Using a wrapper function to add an Image to the GUI

Can we make it so that the dummy text box is created as well?

#Requires AutoHotkey v2.0
#include ImagePut.ahk

image := FileSelect(,, "Select an image:", "Images (*.bmp; *.dib; *.rle; *.jpg; *.jpeg; *.jpe; *.jfif; *.gif; *.emf; *.wmf; *.tif; *.tiff; *.png; *.ico; *.heic; *.hif; *.webp; *avif; *avifs)")

; Make the GUI resizable and not affected by DPI scaling.
; Sets the image filepath as the window title.
app := Gui("-DPIScale +Resize", image)

; Uses ImagePut to load an image into the dummy text control.
AddImageToGui(app, image, "xm+0")

app.Show("xCenter y0 AutoSize")

; User Interaction with the image is disabled by default.
AddImageToGui(gui, image, options, text:="") {
    static WS_CHILD                  := 0x40000000   ; Creates a child window.
    static WS_VISIBLE                := 0x10000000   ; Show on creation.
    static WS_DISABLED               :=  0x8000000   ; Disables Left Click to drag.
    ImagePut.gdiplusStartup()
    pBitmap := ImagePutBitmap(image)
    DllCall("gdiplus\GdipGetImageWidth", "ptr", pBitmap, "uint*", &width:=0)
    DllCall("gdiplus\GdipGetImageHeight", "ptr", pBitmap, "uint*", &height:=0)
    display := Gui.Add("Text", options " w" width " h" height, text)
    display.imagehwnd := ImagePut.show(pBitmap,, [0, 0], WS_CHILD | WS_VISIBLE | WS_DISABLED,, display.hwnd)
    ImagePut.gdiplusShutdown()
    return display
}

Now the above version is perfect. It uses ImagePut's internal functions to load GDI+. This makes raw pointers to bitmaps pBitmap valid within the context of this function. Unlike Example #1, which loads the image 3 times for ImageWidth, ImageHeight, and ImageShow, this function converts the image only once.

Can we do better?

Method #3 - Extending the built in GUI class with AddImage(image)

#Requires AutoHotkey v2.0
#include ImagePut.ahk

class Gui2 extends Gui {
    AddImage(image, options, text:="") {
        static WS_CHILD                  := 0x40000000   ; Creates a child window.
        static WS_VISIBLE                := 0x10000000   ; Show on creation.
        static WS_DISABLED               :=  0x8000000   ; Disables Left Click to drag.
        ImagePut.gdiplusStartup()
        pBitmap := ImagePutBitmap(image)
        DllCall("gdiplus\GdipGetImageWidth", "ptr", pBitmap, "uint*", &width:=0)
        DllCall("gdiplus\GdipGetImageHeight", "ptr", pBitmap, "uint*", &height:=0)
        display := this.Add("Text", options " w" width " h" height, text)
        display.imagehwnd := ImagePut.show(pBitmap,, [0, 0], WS_CHILD | WS_VISIBLE | WS_DISABLED,, display.hwnd)
        ImagePut.gdiplusShutdown()
        return display
    }
}

image := FileSelect(,, "Select an image:", "Images (*.bmp; *.dib; *.rle; *.jpg; *.jpeg; *.jpe; *.jfif; *.gif; *.emf; *.wmf; *.tif; *.tiff; *.png; *.ico; *.heic; *.hif; *.webp; *avif; *avifs)")

; Make the GUI resizable and not affected by DPI scaling.
; Sets the image filepath as the window title.
app := Gui2("-DPIScale +Resize", image)

; Uses ImagePut to load an image into the dummy text control.
app.AddImage(image, "xm+0")

app.Show("xCenter y0 AutoSize")

Creates an extended version of the Gui class. The only thing to remember is to use Gui2 instead of Gui for the method AddImage.

Conclusion

Pikachu

Image: https://github.com/iseahound/ImagePut/assets/9779668/aee6c301-44f1-4fad-8afb-fffef4f90df8

The reason why 3 separate methods are shown is because there is not a single ideal method of extending the native AutoHotkey GUI. Instead, you may choose to use any of the above 3 methods according to your own desires.

Finally, here is the code for the dancing GIF above:

#Requires AutoHotkey v2.0
#include ImagePut.ahk

MyGui := Gui("-DPIScale +Resize")
MyBtn := MyGui.Add("Button", "default", "&Load New Image")
MyBtn.OnEvent("Click", LoadNewImage)
MyRadio := MyGui.Add("Radio", "ym+5 x+10 checked", "Load &actual size")
MyGui.Add("Radio", "ym+5 x+10", "Load to &fit screen")
MyPic := MyGui.Add("Text", "xm")
MyGui.Show()

LoadNewImage(*)
{
    Image := FileSelect(,, "Select an image:", "Images (*.bmp; *.dib; *.rle; *.jpg; *.jpeg; *.jpe; *.jfif; *.gif; *.emf; *.wmf; *.tif; *.tiff; *.png; *.ico; *.heic; *.hif; *.webp; *avif; *avifs)")
    if Image = ""
        return
    if (MyRadio.Value)  ; Display image at its actual size.
    {
        Width := 0
        Height := 0
    }
    else ; Second radio is selected: Resize the image to fit the screen.
    {
        Width := A_ScreenWidth - 28  ; Minus 28 to allow room for borders and margins inside.
        Height := -1  ; "Keep aspect ratio" seems best.
    }
    static imagehwnd := 0
    if imagehwnd
        ImageDestroy(imagehwnd)
    MyGui.Title := Image
    imagehwnd := ImageShow(Image,, [0, 0, Width, Height], 0x58000000,, MyPic.hwnd)
    MyPic.move(,, ImageWidth(imagehwnd), ImageHeight(imagehwnd))
    MyGui.Show("xCenter y0 AutoSize")  ; Resize the window to match the picture size.
}

Bonus: Synchronize Animations

3 Aqua

To synchronize animations, set the last parameter of ImageShow to False. Then, send message 0x8001 to all the frozen image handles.

#Requires AutoHotkey v2.0
#include ImagePut.ahk

image := FileSelect(,, "Select an image:", "Images (*.bmp; *.dib; *.rle; *.jpg; *.jpeg; *.jpe; *.jfif; *.gif; *.emf; *.wmf; *.tif; *.tiff; *.png; *.ico; *.heic; *.hif; *.webp; *avif; *avifs)")

; Make the GUI resizable and not affected by DPI scaling.
app := Gui("-DPIScale +Resize")

; Sets the image filepath as the window title.
app.Title := image

; Create a dummy text control to repurpose for ImagePut's functionality.
display := app.Add("Text", "xm+0")

; Get the width and height.
width := ImageWidth(image)
height := ImageHeight(image)

; Must resize the viewable area of the control.
display.move(,, 3*width, height) ; Note: Extend viewable width by 3!!!

; Use ImagePut to create a child window, and set the parent as the text control.
image_hwnd1 := ImageShow(image,, [0, 0], 0x40000000 | 0x10000000 | 0x8000000,, display.hwnd, False)
image_hwnd2 := ImageShow(image,, [width, 0], 0x40000000 | 0x10000000 | 0x8000000,, display.hwnd, False)
image_hwnd3 := ImageShow(image,, [2*width, 0], 0x40000000 | 0x10000000 | 0x8000000,, display.hwnd, False)

; Show the image
app.Show("xCenter y0 AutoSize")

; Some useful functions.
Play(hwnd) => PostMessage(0x8001,,,, hwnd)
Restart(hwnd) => PostMessage(0x8001, 1,,, hwnd)
Pause(hwnd) => PostMessage(0x8002,,,, hwnd)
Stop(hwnd) => PostMessage(0x8002, 1,,, hwnd)
PlayPause(hwnd) => PostMessage(0x202,,,, hwnd)
RestartStop(hwnd) => PostMessage(0x202, 1,,, hwnd)
IsPlaying(hwnd) => DllCall("GetWindowLong", "ptr", hwnd, "int", 4*A_PtrSize, "ptr")
Step(hwnd, n) => PostMessage(0x8000, n,,, hwnd)
NextFrame(hwnd) => Step(hwnd, 1)
PrevFrame(hwnd) => Step(hwnd, -1)

; Use PostMessage to asynchronously start playback!
Play("ahk_id" image_hwnd1)
Play("ahk_id" image_hwnd2)
Play("ahk_id" image_hwnd3)

; Press F1 to Play, F2 to Pause, and F3 to Stop.
F1:: Play("ahk_id" image_hwnd1)
F2:: Pause("ahk_id" image_hwnd1)
F3:: Stop("ahk_id" image_hwnd1)
F4:: PrevFrame("ahk_id" image_hwnd1)
F5:: NextFrame("ahk_id" image_hwnd1)