Implementação de Barras de Rolagem em Interfaces AHK v2

Visão Geral

Este guia demonstra como implementar barras de rolagem em interfaces gráficas utilizando AutoHotkey v2. A solução apresentada é uma adaptação modernizada de código originalmente desenvolvido para a versão 1, convertendo a sintaxe e aproveitando os recursos orientados a objetos do AHK v2.

Conceito Arquitetural

A estratégia adota uma estrutura hierárquica de jenelas: um container externo recebe as barras de rolagem, enquanto o conteúdo interativo reside em uma janela filha aninhada. Essa abordagem permite que o sistema operacional gerencie nativamente o comportamento de rolagem, mantendo a responsividade da interface.

Implementação da Classe

A classe RolavelGUI encapsula toda a lógica necessária. Segue a implementação completa compatível com AHK v2:

#Requires AutoHotkey v2.0

; ============================================================
; Classe:        RolavelGUI
; Descrição:    Container com capacidade de rolagem para GUIs
; Versão:       2.0.0 (adaptado para AHK v2)
; Base:         Código original de "just me" (AHK v1)
; Licença:      The Unlicense
; ============================================================
class RolavelGUI {
    static ativos := {}
    
    ; Constantes de estilo de janela
    static ESTILO_H := 0x100000, ESTILO_V := 0x200000
    
    __Novo(HandleFilho, Largura, Altura, Opcoes := "", Barras := 3, Roda := 0) {
        ; Validação de parâmetros
        Barras &= 3, Roda &= 7
        if (!this._ValidarParametros(Barras, Roda))
            return false
            
        if !DllCall("User32.dll\IsWindow", "Ptr", HandleFilho, "UInt")
            return false
            
        ; Configura janela filha
        if !this._CalcularDimensoes(HandleFilho, &LarguraFilho, &AlturaFilho)
            return false
            
        guiFilho := GuiFromHwnd(HandleFilho)
        guiFilho.Opt("-Caption -Resize")
        guiFilho.Show(Format("w{} h{} Hide", LarguraFilho, AlturaFilho))
        
        ; Limites e métricas de rolagem
        MaxH := LarguraFilho, MaxV := AlturaFilho
        PassoH := Ceil(MaxH / 20), PassoV := Ceil(MaxV / 20)
        
        ; Ajusta dimensões do container
        if (Largura = 0) || (Largura > MaxH)
            Largura := MaxH
        if (Altura = 0) || (Altura > MaxV)
            Altura := MaxV
            
        ; Cria container com estilos apropriados
        Estilos := (Barras & 1 ? " +" . this.ESTILO_H : "") 
                 . (Barras & 2 ? " +" . this.ESTILO_V : "")
        
        container := Gui(Opcoes . Estilos)
        HwndContainer := container.Hwnd
        container.Show(Format("w{} h{} Hide", Largura, Altura))
        container.Opt(Format("+MaxSize{}x{}", MaxH, MaxV))
        
        ; Armazena referências
        this.container := container
        this.Hwnd := HwndContainer
        this.HandleFilho := HandleFilho
        this.Largura := Largura
        this.Altura := Altura
        this.UsarShift := this.TemH := this.TemV := false
        
        ; Configura barra horizontal
        if (Barras & 1) {
            this._ConfigurarBarra(0, MaxH, Largura + 1, 0x0114, Roda, 1)
            this.MaxH := MaxH
            this.PassoH := PassoH
            this.PaginaH := Largura + 1
            this.PosH := 0
            this.TemH := true
        }
        
        ; Configura barra vertical
        if (Barras & 2) {
            this._ConfigurarBarra(1, MaxV, Altura + 1, 0x0115, Roda, 2)
            this.MaxV := MaxV
            this.PassoV := PassoV
            this.PaginaV := Altura + 1
            this.PosV := 0
            this.TemV := true
        }
        
        ; Anexa filho ao container
        guiFilho.Opt("+parent" . HwndContainer)
        guiFilho.Show("x0 y0")
        
        ; Registra instância e configura redimensionamento
        RolavelGUI.ativos.%HwndContainer% := this
        this._AjustarDimensoes()
        OnMessage(0x0005, (p*) => this._AoRedimensionar(p*))
    }
    
    _ValidarParametros(Barras, Roda) {
        return ((Barras >= 1) && (Barras <= 3)) 
            && ((Roda >= 0) && (Roda <= 4))
    }
    
    _ConfigurarBarra(Tipo, Maximo, Pagina, MsgScroll, Roda, MascaraRoda) {
        this._DefinirInfoScroll(Tipo, { Max: Maximo, Page: Pagina, Pos: 0 })
        OnMessage(MsgScroll, (p*) => this._ProcessarScroll(p*))
        
        if (Roda & 1) && (MascaraRoda & 1)
            OnMessage(0x020E, (p*) => this._ProcessarRoda(p*))
        else if (Roda & 4) {
            OnMessage(0x020A, (p*) => this._ProcessarRoda(p*))
            this.UsarShift := true
        }
        
        if (Roda & 6) && (MascaraRoda & 2)
            OnMessage(0x020A, (p*) => this._ProcessarRoda(p*))
    }
    
    Mostrar(Titulo := "", OpcoesExibicao := "") {
        OpcoesExibicao := RegExReplace(OpcoesExibicao, "i)\+?AutoSize")
        this.container.Show(OpcoesExibicao . " w" . this.Largura . " h" . this.Altura)
        WinSetTitle(Titulo, this.container)
        return true
    }
    
    Destruir() {
        if RolavelGUI.ativos.HasProp(this.Hwnd) {
            GuiFromHwnd(this.Hwnd).Destroy()
            RolavelGUI.ativos.DeleteProp(this.Hwnd)
            return true
        }
        return false
    }
    
    __Delete() {
        this.Destruir()
    }
    
    SincronizarFilho() {
        RC := Buffer(16, 0)
        DllCall("User32.dll\GetWindowRect", "Ptr", this.HandleFilho, "Ptr", RC.Ptr)
        
        LarguraAntiga := NumGet(RC, 8, "Int") - NumGet(RC, 0, "Int")
        AlturaAntiga := NumGet(RC, 12, "Int") - NumGet(RC, 4, "Int")
        
        DllCall("User32.dll\ScreenToClient", "Ptr", this.Hwnd, "Ptr", RC.Ptr)
        XAtual := XNovo := NumGet(RC, 0, "Int")
        YAtual := YNovo := NumGet(RC, 4, "Int")
        
        if !this._CalcularDimensoes(this.HandleFilho, &NovaLargura, &NovaAltura)
            return false
            
        guiFilho := GuiFromHwnd(this.HandleFilho)
        guiFilho.Show(Format("x{} y{} w{} h{}", XAtual, YAtual, NovaLargura, NovaAltura))
        
        this.container.Opt(Format("+MaxSize{}x{}", NovaLargura, NovaAltura))
        
        if (NovaLargura < this.Largura) || (NovaAltura < this.Altura) {
            guiFilho.Show("w" . NovaLargura . " h" . NovaAltura)
            this.Largura := NovaLargura
            this.Altura := NovaAltura
            this._DefinirPagina(1, NovaLargura + 1)
            this._DefinirPagina(2, NovaAltura + 1)
        }
        
        PassoH := Ceil(NovaLargura / 20)
        PassoV := Ceil(NovaAltura / 20)
        
        if this.TemH {
            this._DefinirMaximo(1, NovaLargura)
            this.PassoH := PassoH
            if (XAtual + NovaLargura) < this.Largura {
                XNovo += this.Largura - (XAtual + NovaLargura)
                XNovo := Max(XNovo, 0)
                this._DefinirInfoScroll(0, { Pos: XNovo * -1 })
                this._ObterInfoScroll(0, &SI)
                this.PosH := NumGet(SI, 20, "Int")
            }
        }
        
        if this.TemV {
            this._DefinirMaximo(2, NovaAltura)
            this.PassoV := PassoV
            if (YAtual + NovaAltura) < this.Altura {
                YNovo += this.Altura - (YAtual + NovaAltura)
                YNovo := Max(YNovo, 0)
                this._DefinirInfoScroll(1, { Pos: YNovo * -1 })
                this._ObterInfoScroll(1, &SI)
                this.PosV := NumGet(SI, 20, "Int")
            }
        }
        
        if (XAtual != XNovo) || (YAtual != YNovo)
            DllCall("User32.dll\ScrollWindow", "Ptr", this.Hwnd, 
                "Int", XNovo - XAtual, "Int", YNovo - YAtual, "Ptr", 0, "Ptr", 0)
        
        return true
    }
    
    _DefinirMaximo(Barra, Valor) {
        Barra-- ; Converte 1/2 para 0/1
        if (Barra != 0) && (Barra != 1)
            return false
        (Barra = 0) ? (this.MaxH := Valor) : (this.MaxV := Valor)
        return this._DefinirInfoScroll(Barra, { Max: Valor })
    }
    
    _DefinirPagina(Barra, Valor) {
        Barra--
        if (Barra != 0) && (Barra != 1)
            return false
        (Barra = 0) ? (this.PaginaH := Valor) : (this.PaginaV := Valor)
        return this._DefinirInfoScroll(Barra, { Page: Valor })
    }
    
    _CalcularDimensoes(Handle, &LarguraTotal, &AlturaTotal) {
        EstadoAnterior := A_DetectHiddenWindows
        DetectHiddenWindows(1)
        
        Retangulo := Buffer(16, 0)
        LarguraTotal := AlturaTotal := 0
        
        HandleAtual := Handle
        Comando := 5 ; GW_CHILD
        Esquerda := Topo := Direita := Base := EsqOculto := TopoOculto := ""
        
        while (HandleAtual := DllCall("GetWindow", "Ptr", HandleAtual, "UInt", Comando, "UPtr")) 
              && (Comando := 2) {
            WinGetPos(&X, &Y, &W, &H, HandleAtual)
            
            PosDireita := X + W
            PosBase := Y + H
            
            Estilos := WinGetStyle(HandleAtual)
            if (Estilos & 0x10000000) { ; WS_VISIBLE
                if (Esquerda = "") || (X < Esquerda)
                    Esquerda := X
                if (Topo = "") || (Y < Topo)
                    Topo := Y
                if (Direita = "") || (PosDireita > Direita)
                    Direita := PosDireita
                if (Base = "") || (PosBase > Base)
                    Base := PosBase
            } else {
                if (EsqOculto = "") || (X < EsqOculto)
                    EsqOculto := X
                if (TopoOculto = "") || (Y < TopoOculto)
                    TopoOculto := Y
            }
        }
        
        DetectHiddenWindows(EstadoAnterior)
        
        ; Converte coordenadas de tela para cliente
        Ponto := Buffer(8, 0)
        if (EsqOculto != "") {
            NumPut("Int", EsqOculto, Ponto, 0)
            DllCall("ScreenToClient", "Ptr", Handle, "Ptr", Ponto.Ptr)
            EsqOculto := NumGet(Ponto, 0, "Int")
        }
        if (TopoOculto != "") {
            NumPut("Int", TopoOculto, Ponto, 4)
            DllCall("ScreenToClient", "Ptr", Handle, "Ptr", Ponto.Ptr)
            TopoOculto := NumGet(Ponto, 4, "Int")
        }
        
        NumPut("Int", Esquerda, Retangulo, 0)
        NumPut("Int", Topo, Retangulo, 4)
        NumPut("Int", Direita, Retangulo, 8)
        NumPut("Int", Base, Retangulo, 12)
        
        DllCall("MapWindowPoints", "Ptr", 0, "Ptr", Handle, "Ptr", Retangulo.Ptr, "UInt", 2)
        
        LarguraTotal := NumGet(Retangulo, 8, "Int") 
                      + (EsqOculto != "" ? EsqOculto : NumGet(Retangulo, 0, "Int"))
        AlturaTotal := NumGet(Retangulo, 12, "Int") 
                     + (TopoOculto != "" ? TopoOculto : NumGet(Retangulo, 4, "Int"))
        
        return true
    }
    
    _ObterInfoScroll(Barra, &Info) {
        Info := Buffer(28, 0)
        NumPut("UInt", 28, Info, 0)
        NumPut("UInt", 0x17, Info, 4) ; SIF_ALL
        return DllCall("User32.dll\GetScrollInfo", "Ptr", this.Hwnd, 
            "Int", Barra, "Ptr", Info.Ptr, "UInt")
    }
    
    _DefinirInfoScroll(Barra, Valores) {
        static MASCARAS := { Max: 0x01, Page: 0x02, Pos: 0x04 }
        static OFFSETS := { Max: 12, Page: 16, Pos: 20 }
        
        Mascara := 0
        Info := Buffer(28, 0)
        NumPut("UInt", 28, Info, 0)
        
        for Chave, Valor in Valores.OwnProps() {
            if MASCARAS.HasProp(Chave) {
                Mascara |= MASCARAS.%Chave%
                NumPut("UInt", Valor, Info, OFFSETS.%Chave%)
            }
        }
        
        if (Mascara) {
            NumPut("UInt", Mascara | 0x08, Info, 4) ; SIF_DISABLENOSCROLL
            return DllCall("User32.dll\SetScrollInfo", "Ptr", this.Hwnd, 
                "Int", Barra, "Ptr", Info.Ptr, "UInt", 1, "UInt")
        }
        return false
    }
    
    _ProcessarScroll(WP, LP, Msg, Hwnd) {
        if !RolavelGUI.ativos.HasProp(Hwnd)
            return
            
        Instancia := RolavelGUI.ativos.%Hwnd%
        if ((Msg = 0x0114) && Instancia.TemH) || ((Msg = 0x0115) && Instancia.TemV)
            return Instancia._ExecutarScroll(WP, LP, Msg, Hwnd)
    }
    
    _ExecutarScroll(WP, LP, Msg, Hwnd) {
        static ACoes := { 0: "LINEMINUS", 1: "LINEPLUS", 
                         2: "PAGEMINUS", 3: "PAGEPLUS", 5: "THUMBTRACK" }
        
        if (LP != 0)
            return
            
        Barra := (Msg = 0x0114) ? 0 : 1
        Codigo := WP & 0xFFFF
        Deslocamento := (Msg = 0x0114) ? this.PassoH : this.PassoV
        
        if !this._ObterInfoScroll(Barra, &Info)
            return
            
        PosAtual := PosNova := NumGet(Info, 20, "Int")
        TamanhoPagina := NumGet(Info, 16, "UInt")
        
        switch Codigo {
            case 0: PosNova := PosAtual - Deslocamento
            case 1: PosNova := PosAtual + Deslocamento
            case 2: PosNova := PosAtual - TamanhoPagina
            case 3: PosNova := PosAtual + TamanhoPagina
            case 5: PosNova := NumGet(Info, 24, "Int")
        }
        
        if (PosAtual = PosNova)
            return 0
            
        this._DefinirInfoScroll(Barra, { Pos: PosNova })
        this._ObterInfoScroll(Barra, &Info)
        PosNova := NumGet(Info, 20, "Int")
        
        (Barra = 0) ? (this.PosH := PosNova) : (this.PosV := PosNova)
        
        if (PosAtual != PosNova) {
            DeslH := (Msg = 0x0114) ? PosAtual - PosNova : 0
            DeslV := (Msg = 0x0115) ? PosAtual - PosNova : 0
            DllCall("User32.dll\ScrollWindow", "Ptr", this.Hwnd, 
                "Int", DeslH, "Int", DeslV, "Ptr", 0, "Ptr", 0)
        }
        return 0
    }
    
    _AoRedimensionar(WP, LP, Msg, Hwnd) {
        if ((WP = 0) || (WP = 2)) && RolavelGUI.ativos.HasProp(Hwnd) {
            Largura := LP & 0xFFFF
            Altura := (LP >> 16) & 0xFFFF
            return RolavelGUI.ativos.%Hwnd%._AjustarDimensoes(Largura, Altura)
        }
    }
    
    _AjustarDimensoes(Largura := 0, Altura := 0) {
        if (Largura = 0) || (Altura = 0) {
            RC := Buffer(16, 0)
            DllCall("User32.dll\GetClientRect", "Ptr", this.Hwnd, "Ptr", RC.Ptr)
            Largura := NumGet(RC, 8, "Int")
            Altura := NumGet(RC, 12, "Int")
        }
        
        DeslH := DeslV := 0
        
        if this.TemH && (Largura != this.Largura) {
            this._DefinirInfoScroll(0, { Page: Largura + 1 })
            this.Largura := Largura
            this._ObterInfoScroll(0, &Info)
            NovaPosH := NumGet(Info, 20, "Int")
            DeslH := this.PosH - NovaPosH
            this.PosH := NovaPosH
        }
        
        if this.TemV && (Altura != this.Altura) {
            this._DefinirInfoScroll(1, { Page: Altura + 1 })
            this.Altura := Altura
            this._ObterInfoScroll(1, &Info)
            NovaPosV := NumGet(Info, 20, "Int")
            DeslV := this.PosV - NovaPosV
            this.PosV := NovaPosV
        }
        
        if (DeslH) || (DeslV)
            DllCall("User32.dll\ScrollWindow", "Ptr", this.Hwnd, 
                "Int", DeslH, "Int", DeslV, "Ptr", 0, "Ptr", 0)
        
        return 0
    }
    
    _ProcessarRoda(WP, LP, Msg, Hwnd) {
        HwndAtivo := WinActive("A") + 0
        
        if (HwndAtivo != Hwnd) && RolavelGUI.ativos.HasProp(HwndAtivo) {
            Instancia := RolavelGUI.ativos.%HwndAtivo%
            SobreBarra := SendMessage(0x0084, 0, LP & 0xFFFFFFFF, , HwndAtivo)
            
            if (SobreBarra = 6) && Instancia.TemH
                return Instancia._ExecutarRoda(WP, LP, 0x020E, HwndAtivo)
            if (SobreBarra = 7) && Instancia.TemV
                return Instancia._ExecutarRoda(WP, LP, 0x020A, HwndAtivo)
        }
        
        if RolavelGUI.ativos.HasProp(Hwnd) {
            Instancia := RolavelGUI.ativos.%Hwnd%
            if ((Msg = 0x020E) && Instancia.TemH)
                || ((Msg = 0x020A) && (Instancia.TemV 
                    || (Instancia.TemH && Instancia.UsarShift && (WP & 0x0004))))
                return Instancia._ExecutarRoda(WP, LP, Msg, Hwnd)
        }
    }
    
    _ExecutarRoda(WP, LP, Msg, Hwnd) {
        if (Msg = 0x020A) && this.UsarShift && (WP & 0x0004)
            Msg := 0x020E
            
        MsgScroll := (Msg = 0x020A) ? 0x0115 : 0x0114
        Direcao := ((WP >> 16) > 0x7FFF) || (WP < 0) ? 1 : 0
        
        return this._ExecutarScroll(Direcao, 0, MsgScroll, Hwnd)
    }
}

Exemplos Práticos

Exemplo 1: Uso Básico

#Include RolavelGUI.ahk

Esc::ExitApp()

; Cria interface principal
janelaPrincipal := Gui("+Resize")
janelaPrincipal.BackColor := "f1ffb1"
janelaPrincipal.AddText("x0 y0", "Demonstração")

; Constrói conteúdo extenso
conteudo := Gui()
Loop 100 {
    linha := A_Index
    Loop 5 {
        if A_Index = 1
            conteudo.AddText("xm y+10 w21 h21 0x201", linha)
        else
            conteudo.AddText("xm y+10 w21 h21 yp 0x201", linha)
        conteudo.AddEdit(" x+10 yp w55 hp r1", A_Index)
    }
}

; Instancia container rolável (altura máxima de 100px)
rolavel := RolavelGUI(conteudo.Hwnd, 0, 100, "-Caption -Resize", 2, 2)
rolavel.Mostrar("Interface Rolável", "x0 y100 NA")

; Embede no container principal
rolavel.container.Opt("+Parent" . janelaPrincipal.Hwnd)
DllCall("SetParent", "Ptr", rolavel.container.Hwnd, "Ptr", janelaPrincipal.Hwnd)
janelaPrincipal.Show("w300 h400")

Exemplo 2: Sincronização Dinâmica

janelaDinamica := Gui()
janelaDinamica.AddButton("x0 y0 w80 h20", "Alterar Tamanho")
    .OnEvent("Click", Atualizar)

Loop 10 {
    linha := A_Index
    Loop 5 {
        if A_Index = 1
            janelaDinamica.AddText("xm y+10 w22 0x201", linha)
        else
            janelaDinamica.AddText("x+10 w22 yp 0x201", linha)
        janelaDinamica.AddEdit("x+10 yp w55 hp r1", A_Index)
    }
}

rolavelDinamico := RolavelGUI(janelaDinamica.Hwnd, 330, 200, "Caption Resize", 3, 4)
rolavelDinamico.Mostrar("Rolagem Dinâmica", "x100 y500 NA")

Atualizar(*) {
    rolavelDinamico.container.Move(,, 30)
    rolavelDinamico.SincronizarFilho()
}

Exemplo 3: Controle Programático

painel := Gui()
painel.SetFont("s15")
painel.AddText("x0 y0 w200 h100")

Loop 60 {
    painel.AddText("x0 yp+20 h20 Border Backgroundc8ffcb", A_Index)
}
painel.BackColor := "e5ffee"

rolavelManual := RolavelGUI(painel.Hwnd, 200, 200, "-Caption -Resize", 2, 2)
rolavelManual.SetLine(2, 20)
rolavelManual.Mostrar("Controle Manual", "x500 y500 NA")

referencia := rolavelManual.container

; Atalhos para navegação
a:: rolavelManual._ProcessarScroll(1, 0, 0x0115, referencia.Hwnd)
d:: rolavelManual._ProcessarScroll(0, 0, 0x0115, referencia.Hwnd)

q:: {
    while rolavelManual.PosV > 1
        SendMessage(0x0115, 2, 0, rolavelManual.container)
}

e:: {
    while rolavelManual.MaxV - rolavelManual.PaginaV + 1 > rolavelManual.PosV
        SendMessage(0x0115, 3, 0, rolavelManual.container)
}

Referência de Parâmetros

Parâmetro Descrição Valores
Barras Tipo de barras de rolagem 1=horizontal, 2=vertical, 3=ambas
Roda Comportamento da roda do mouse 0=desativado, 1=horizontal, 2=vertical, 3=ambas, 4=vertical+Shift
Largura/Altura Dimensões visíveis 0=automático (baseado no conteúdo)

Observações Importantes

  • As dimensões especificadas nunca excedem o tamanho real do conteúdo
  • Utilize SincronizarFilho() após modificar controles dinamicamente
  • O modificador Ctrl permite rolagem mesmo com controles focados
  • Para ocultar barras visualmente, utilize WinSetRegion() no container

Tags: AutoHotkey AHK v2 gui scrollbar Win32 API

Publicado em 8-5 05:34