Visual Lisp e DCL com a linha de comando

Bom dia!!

Ah, sim e feliz ano novo!!!

Prometo tentar retomar as postagens no blog, até porque, preciso levantar a audiência, já que meus parceiros resolveram me furar o olho. Sim estou falando com vocês.

Pra começar o ano, vamos retomar um pouco das raízes deste blog, falando um pouco de Visual Lisp.

Faço parte de um grupo de whatsapp que fala deste tema em particular - é existe um, vai entender - que outro dia estava uma discussão enorme sobre como fazer uma lisp mostrar um diálogo e neste ter um botão para clicar um ponto na tela do cad e tal.

Bem, apesar das explicações mais bem elaboradas que se podia dar, nada como um exemplo prático para mostrar uma das soluções encontradas!!!

Abaixo você verá isso, mas antes algumas considerações:

Depois que você abre um diálogo em visual lisp ( o dcl ), não pode acessar a linha de comando.

Mas por que?

Bem, digamos que o mecanismo que interpreta o Visual Lisp no AutoCAD não está preparado para isso, como acontece na API DOTNET e simplesmente dá erro fatal....

Mas o que eu quero dizer com acessar a linha de comando?

Bem, usar qualquer função do Visual Lisp que interaja com a linha de comando, uai, por exemplo: getint, getreal. getpoint, ssget, command....

Mas se mesmo assim eu precisar disso, como no exemplo, que vamos estudar?

Então a solução é fechar o diálogo (DCL) antes de acessar a linha de comando e como precisaremos voltar para o DCL após ter interagido com a linha de comando, vamos incluir um looping que fique mostrando este DCL até que queiramos parar.

A ideia fica assim:

Inicializar as variáveis
iniciar o looping
   iniciar o DCL
   mostrar o DCL e monitorar as ações nele
   se clicou num botão, decida:
       clicou OK, sai do looping, com status de OK, pois o usuário aceita os valores dos campos
       clicou CANCELAR, sai do looping com o status CANCELAR
             então restaura os valores originais
       clicou um botão diferente, então faz o que tem de fazer e faz looping outra vez
volta para o looping até que tenha de sair dele

Como algorítimo, o exemplo se resume a isso.

Durante o tempo que as postagens no grupo do whatsapp, outros problemas foram adicionados e um deles era atualizar um SLIDE ao clicar uma opção num RADIO_BUTTON do DCL.

Outro, era validar os valores que eram digitados neste DCL, por exemplo, existem dois campos (X e Y) que esperam valores numéricos, pois representam uma coordenada, sendo assim, não pode conter um valor alfanumérico.

Já que ia mostrar um SLIDE neste DCL, resolvi incluir mais um item: clicar na imagem.
No exemplo, isso não tem uma utilidade, a não ser demonstrar o conceito. Eu uso este conceito no EXPGE, na hora que você clica o mapa para escolher o fuso:


Eu gosto de separar todas as ações de cliques do DCL numa subrotina que a processa, assim, evito de escrever as ações no DCL com o "action_tile", o que me parece bem mais racional, principalmente se você escrever um DCL para cada idioma que seu aplicativo puder mostrar. Por exemplo, o EXPGE e o MLH2 mostram o diálogo em português e inglês.

Esta subrotina também pode validar o valor que pretendo modificar na tela do DCL, assim, se tentar escrever um texto qualquer no campo que espera conter um número, este pode informar um erro, ou mesmo corrigir para o valor inicial.

Isto se faz assim no exemplo:

 ( subrotinaQueProcessaAcao key valor razao posx posy / ... )

Veja, asubrotina recebe 5 parâmetros, que são passados a ela sempre que algo é clicado, alterado ou escolhido no DCL. Convém ler a ajuda do AutoCAD. Por hora, aceite isso.

Os parâmetros são:
key - nome do campo que foi clicado/alterado
valor - valor digitado no campo, quando aplicável
razao - indica qual ação o usuário fez após digitar: clocou outro campo, clicou ENTER, etc...
posx - ordenada X da posição onde o mouse clicou uma imagem
posy - ordenada Y

Vamos melhorar o algorítimo para mostrar isso e vamos mostrar uma maneira de armazenar uma lista de valores padrão do ANTES de mostrar eles no DCL e decidir o que fazer com eles se clicar OK ou CANCEL:

Inicializar as variáveis com valores padrão
iniciar o DCL
iniciar o looping
   mostrar o DCL
   preencher os campos
   definir as ações de cada campo
   monitorar as ações nele
   se clicou num botão, decida:
       clicou OK, sai do looping, com status de OK, pois o usuário aceita os valores dos campos
       clicou CANCELAR, sai do looping com o status CANCELAR
             então restaura os valores originais
       clicou um botão diferente, então faz o que tem de fazer e faz looping outra vez
volta para o looping até que tenha de sair dele

Note que inclui duas linhas, marcadas em negrito.
Tente entender porque iniciar as variáveis com valores padrão pode ser útil. Imagine que você usou o programa, modificou valores. Clicou OK. Chamou o programa outra vez, o DCL pode mostrar as últimas escolhas que você fez.

Se este for o comportamento que você espera, terá de armazenar a lista de valores padrão numa variável global. Siga o link!!!

Bem, agora que o algorítimo básico foi mostrado, vamos ver o código. Comecemos com a "cara" que fica o DCL:


Note o "Radio" era uma das coisas que pediam: ter um RADIO_BUTTON que, ao ser clicado, trocasse a "imagem" do campo ao lado, então ao clicar outra opção, outra imagem é exibida.

No exemplo, X e Y começam com valor zero. A opção inicial é "C" no campo "radio" e a escala da imagem é 1. Então comece com isso:

;valores padrão das variáveis globais
;é uma lista com pares de ("nomeDaVariavel" "valorDaVariavel")
(setq exemplo:listaKeys
       '(("r1" "1")       ;radio button 1
     ("r2" "0")       ;radio button 2
     ("r3" "0")       ;radio button 3
     ("clicapt"  )    ;button "clicapt" precisa sair do diálogo
     ("x" "0.0")      ;ordenada x sendo armazenada
     ("y" "0.0")      ;ordenada y sendo armazenada
     ("escala" "1")   ;escala da imagem no dcl
     ("btcresce")
     (
"btdiminui")
     (
"imagem")
     (
"help")   ))


Uma dica: Observe como nomeio a variável global. Não é obrigatório fazer dessa maneira, mas ajuda a manter o código livre de sobreposições de outros programas. Falo disso depois, ok?

Você pode criar uma variável para cada campo do DCL, mas me parece mais simples assim. Quando você ler o código todo irá concordar. Veja que mesmo os campos numéricos mantive com strings. Isso facilita na hora de preencher o DCL, você vai ver....

Já que estou armazenando os valores dos campos desse DCL numa lista, precisamos de uma subrotina que insira um novo valor nela, pra não ficar repetindo várias linhas de código toda vez que precisamos mudar isso, então aqui está:

;subrotina que seta o valor de uma variavel global
;nomeVariavel => string com o nome da variavel global
;valor => string string com o valor a ser definido
;exemplo: (exemplo->put-valor "x" "0.3")
;saida: a lista de variaveis globais atualizada
(defun exemplo->put-valor (nomeVariavel valor / old new)
  (
setq old               (assoc nomeVariavel exemplo:listaKeys)
    new               (list nomeVariavel valor)
    exemplo:listaKeys (subst new old exemplo:listaKeys)))


Na verdade é bem simples, trata-se de manipular a lista, colocando outro valor num campo qualquer.

Já que criei uma função para modificar a lista, vamos criar uma para "tomar" um valor qualquer:

;subrotina que devolve o valor de uma variavel global
;nomeVariavel => nome da variavel a retornar o valor
;resposta: string, valor a ser devolvido ou NIL se a variavel nao exista
;exemplo: (exemplo->get-valor "x")
;saida: "0.3"
(defun exemplo->get-valor (nomeVariavel)
  (
cadr (assoc nomeVariavel exemplo:listaKeys)))


Bem simples também.

Acredito que será mais produtivo  ver agora o programa principal, para que você entenda o que quero mostrar.

Depois vemos as ações e subrotinas complementares.

Relembrando, o programa principal consiste em:

Inicializar as variáveis com valores padrão
iniciar o DCL
iniciar o looping
   mostrar o DCL
   preencher os campos
   definir as ações de cada campo
   monitorar as ações nele
   se clicou num botão, decida:
       clicou OK, sai do looping, com status de OK, pois o usuário aceita os valores dos campos
       clicou CANCELAR, sai do looping com o status CANCELAR
             então restaura os valores originais
       clicou um botão diferente, então faz o que tem de fazer e faz looping outra vez
volta para o looping até que tenha de sair dele

Agora veja o código:

(defun c:teste (/ dcl dlg faz pt lembrar)

  (
setq dcl  (load_dialog "c:/temp/teste.dcl") ;carrega o dcl e inicializa os valores das tiles
        faz t                                  ;controla o loopin WHILE
    lembrar exemplo:listaKeys)             ;para lembrar os valores se clicou cancel

    

  ;loopinq que mostra o dialogo até clicar OK ou CANCEL
  (while faz

    ;carrega o dialogo desejado
    (new_dialog "teste" dcl )

    ;inicializa as tiles com ação e valor inicial
    (foreach k exemplo:listaKeys
      (action_tile (car k) "(exemplo->acoes $key $value $reason $x $y)" )   ;define a acao
     

      (if (cadr k) (set_tile (car k) (cadr k))))  ;preenche o valor no dialogo

    ;prepara a imagem sem ter clicado um radiobuton
    (exemplo->seta-slide nil)
    

    ;mostra o dialogo na tela e espera clicar algo
    (setq dlg (start_dialog))

    ;clicou um botao qualquer, seja cancel ou OK ou um botão que tem acao que depende da linha de comando
    ;se deprende da linha de comando, precisa fechar o dialogo, senao dá erro fatal
    (cond ((= 0 dlg) ;cancel
       (setq faz nil)
       (
alert "Cancelou! vou restaurar a lista de variaveis globais!")
       (
setq exemplo:listaKeys lembrar))

      ((
= 1 dlg) ;ok
       (setq faz nil)
       (
alert "Clicou OK, vou fechar a tela mano!!"))

      ((
= 2 dlg) ;clicou o botao para capturar um ponto na tela
       (setq pt (getpoint
              (strcat "Informe um ponto <"
                  (exemplo->get-valor "x")
                  ","
                  (exemplo->get-valor "y")
                  ">")))
       (
if pt
         (progn
           (exemplo->put-valor "x" (rtos (car pt) 2 3))
           (
exemplo->put-valor "y" (rtos (cadr pt) 2 3)))))
    )
 ;cond
  );while

  (unload_dialog dcl)

  )


Considerações:
Vou supor que você já saiba ao menos carregar o DCL (load_dialog, start_dialog, new_dialog). Siga os links, não dói, hehehe

Veja a parte de "setar" os campos do DCL. um simples FOREACH resolve. Percebe porque escolhi as variáveis numa lista já como "string"? assim não preciso converter em string um valor que pode ser real, inteiro, string, etc....

Veja que este mesmo FOREACH também define a ação de cada campo com o action_tile bem simples ( "(exemplo->acoes $key $value $reason $x $y)" )

Perceba que o nome do campo é o primeiro elemento da lista de pares nome/valor

Ah, sim, a imagem não tem um valor propriamente dito, então basta deixar vazio na lista de variáveis. Mas precisamos inicializar ela, baseado escolha inicial do RADIO_BUTTON.

Fiz isso com este pedaço do código:
(exemplo->seta-slide nil)

Esta subrotina, espera que você informe qual é a escolha, para que ele mostre a imagem correta.
Ah, convém ver a ajuda de algumas funções (start_image, fill_image, slide_image, end_image) para entender que elas precisam estar em sequencia.

;suprotina que preenche o slide correto na imagem
;exemplo:  (exemplo->seta-slide "r1")
(defun exemplo->seta-slide (radioButon / radioButons nomeSlide largura altura
                larguraReduzida alturaReduzida posX posY escala cor
)
  (
setq radioButons '(("r1" "c:\\temp\\teste(a)") ("r2" "c:\\temp\\teste(b)") ("r3" "c:\\temp\\teste(c)"))
    keyCampo    "imagem"

    ;obtem a escala, precisa estar antes de start_image
    escala      (atof (get_tile "escala")) 

    ;toma o tamanho do retangulo que mostra os slides
        largura (dimx_tile keyCampo)
        altura  (dimy_tile keyCampo)

    ;recalcula o tamanho da janela disposnivel
    larguraReduzida (fix (* largura escala))
    alturaReduzida  (fix (* altura escala))

    ;calcula o canto superior esquerdo onde inserir o slide
    posX    (fix (/ (- largura larguraReduzida)  2))
    posY    (fix (/ (- altura alturaReduzida)  2))

    ;cor da área util onde o slide ira aparecer
    cor      1 ;vermelho
    )
  

  ;encontra o slide apropriado:
  (if radioButon
    ;se informou a opção do radiobuton, pega ele
    (setq nomeSlide (cadr (assoc radioButon radioButons)))

    ;nao informou, significa que é pra procurar qual está setado  por padrao
    (foreach k radioButons
      (if (= "1" (get_tile (car k)))
    (
setq nomeSlide (cadr k)))))
    

  ;inicializa o retangulo que mostra a imagem
  (start_image keyCampo)

  

  ;pinta o background de preto:
  (fill_image 0 0 largura altura 0)

  ;pinta de vermelho a area onde o slide vai aparecer:
  (fill_image posX posY larguraReduzida alturaReduzida cor)
  

  ;desenha o slide na area reduzida
  (slide_image posX posY larguraReduzida alturaReduzida nomeSlide)

  ;finaliza
  (end_image)
  )


Veja que se não informo qual é a opção desejada, esta subrotina procura a escolha atual. Na variável global que armazena a lista de campos, tem três campos: "r1", "r2" e "r3" que são os radio_button. o escolhido é aquele que tem o valor "1"

Tá, eu enfeitei um pouco esta subrotina nesta parte de calcular uma escala, pintar de vermelho e tal. A ideia é mostrar que se pode colocar uma imagem em escala, centralizar etc...

A última parte do código é a subrotina que processa as ações dos campos do DCL. Note que como ela recebe o nome do campo, um COND é indicado para escolher qual ação tomar, em vez de colocar tudo dentro de strings emporcalhando o código do programa principal:

;acoes das tiles.
;key => string, nome do campo no DCL
;valor => string, valor passado pela ação
;razao => inteiro, razao do evento, ver a ajuda
;coordX => quando clica o image_button, informa a ordenada X em pixels
;coordY => quando clica o image_button, informa a ordenada Y em pixels

(defun exemplo->acoes (key valor razao coordX coordY / coleta tmp)
  ;para todas as acoes exceto as que tem done_dialog, coleta os valores
  (setq coleta t)
  

  ;testa qual campo disparou a acao:
  (cond (;algum dos radiobuton? muda de slide!!!!!!!!!!!!!!!!
     (member key '("r1" "r2" "r3")) (exemplo->seta-slide key))

    ;clicou o botao ajuda
    ((eq key "help") (startapp "explorer.exe" "https://tbn2net.com"))
    

    ;trocou a escala
    ((eq key "escala")
     (
set_tile "escala" (if (> (setq tmp (atof valor)) 0)
                  (
rtos tmp 2 3)
                  (
exemplo->get-valor "escala")))
     (
exemplo->seta-slide nil))

    ;apertou o botao de aumetar a escala
    ((eq key "btcresce")
     (
exemplo->acoes "escala" (rtos (* 2 (atof (get_tile "escala"))) 2 3) 1 nil nil))

    ;apertou o botao de diminuir
    ((eq key "btdiminui")
     (
exemplo->acoes "escala" (rtos (* 0.5 (atof (get_tile "escala"))) 2 3) 1 nil nil))

    ;escreveu um valor em X
    ((eq key "x")  (set_tile "x" (rtos (atof valor) 2 3)))

    ;escreveu um valor em Y
    ((eq key "y")  (set_tile "y" (rtos (atof valor) 2 3)))

    ;clicou o botao para acessar a linha de comando e pedir um ponto
    ((eq key "clicapt") (setq coleta nil) (done_dialog 2))

    ;clicou na imagem
    ((eq key "imagem")
     (
alert (strcat "Você clicou dentro da imagem, nas coordenadas "
            (itoa coordX) ","
            (itoa coordY)))))

  ;coleta os novos valores se nao foi um done_dialog:
  (if coleta
    (setq exemplo:listaKeys
       (mapcar '(lambda (k)
              (
list (car k) (get_tile (car k))))
           exemplo:listaKeys)))  )


Bem simples se você ler com calma, verá que pode incluir mais campos apenas acrescentando mais linhas ao COND.

Neste código o truque que quero mostrar é justamente aquele de clicar o botão de obter a coordenada na linha de comando, o botão com ">" no DCL.

Ele faz um DONE_DIALOG. para o botão em questão, usei o [status] igual a 2.
Lembre-se:
OK termina o diálogo com status 1
CANCEL, termina o diálogo com status 0

Então precisamos saber diferenciar quem está encerrando o DCL (diálogo)

Note que incluí uma ação para o HELP, abrindo o navegador e mostrando a minha página. Você pode fazer abrir um arquivo qualquer, com a ajuda do seu programa.

Note a ação que é executada para X e para Y. Ela converte qualquer entrada que você escreve nestes campos para um número real e depois reconverte para string e reescreve esse valor no campo. Isso faz com que o campo só tenha valor numérico.

Note que incluí um pequeno controle usando a variável "coleta". Tive de fazer isso pois preciso lembrar os valores que estão nos campos, reescrevendo a variável global. Mas isso não pode ocorrer ao clicar o botão ">" (key "clicapt") pois o done_dialog anula todos os campos e get_tile retorna nil em todos os campos, o que ia ferrar com DCL.

Bem, depois que você copiar todos os códigos, tente tirar essa verificação, só por farra!!!

Agora vamos ver o famigerado DCL:


teste: dialog {label="exemplo";
:
row {
 :column {
 :radio_column { label = "Radio";
   :
radio_button {label = "A"; key = "r1";}
   :radio_button {label = "B"; key = "r2";}
   :radio_button {label = "C"; key = "r3";}
 }


 :boxed_column {label = "Coordenadas";
   :
edit_box {label = "X" ; key="x";}
   :edit_box {label = "Y" ; key="y";}
   :button {label = ">" ; key = "clicapt";}}
}

:boxed_column {label = "Imagem";
  :
image_button {key = "imagem"; width=50; height =20;
                 fixed_width=true;fixed_height=true;}
  :row { :edit_box {label = "Escala" ; key="escala";}
         :button {label = "+" ; key = "btcresce";}
         :button {label = "-" ; key = "btdiminui";} }
    
  }}

:text {label = "Powered by Neyton";
ok_cancel_help;
   }


Note que para mostrar os botões OK, CANCEL e HELP usei a definição padrão deles. Sim é possível mudar isso, basta criar botões personalizados, mas isso fica pra outro post.

Vamos olhar o programa principal novamente. Olhe o que acontece depois que fazemos o START_DIALOG.

Temos um COND que escolhe uma ação, baseado no STATUS passado a ele:
status 1, OK, sai do looping e mantem as alterações da variável global
status 0, CANCEL, sai do looping e restaura os valores originais da variável global
status 2, mostra a linha de comando e pede que clique uma coordenada e depois volte a mostrar o DCL

Pronto. Estude o código. Dica: leia primeiro somente os cometários do código. Depois leia o código.

Veja, este exemplo mostra uma maneira de oferecer ao usuário uma forma mais amigável de modificar opções no programa que criamos. Isso é preferível quando temos uma grande quantidade de opções alteráveis.

Eu prefiro evitar DCL quando se trata de um ou dois campos. Prefiro dar as opções na linha de comando. Mas isso é questão de gosto....

Falta ainda:
Controle de erros
Compilar para FAS/VLX

Tá com preguiça de copiar e colar, então baixa aqui!!!

Note que salvei o exemplo em C:/temp/, então pra rodar, salve neste local ou edite os caminhos. Os slides, dcl e código estão no pacote zipado do link acima.