Morphic no Pharo Smalltalk

Traduzido de Morphic.

1 Morphic

Morphic é o nome dado à interface gráfica do Pharo. Morphic suporta dois aspectos principais: por um lado Morphic define todas as entidades gráficas de baixo nível e infra-estrutura relacionada (eventos, desenho,…) e por outro lado Morphic define todos os widgets disponíveis no Pharo. O Morphic é escrito em Pharo, portanto é totalmente portátil entre os sistemas operacionais. Como conseqüência, o Pharo parece exatamente o mesmo no Unix, MacOS e Windows. O que distingue o Morphic da maioria dos outros kits de interface de usuário é que ele não possui modos separados para compor e executar a interface: todos os elementos gráficos podem ser montados e desmontados pelo usuário, a qualquer momento. Agradecemos a Hilaire Fernandes pela permissão de basear este capítulo em seu artigo original em francês.

1.1 A história do Morphic

Morphic foi desenvolvido por John Maloney e Randy Smith para a linguagem de programação Self, a partir de aproximadamente 1993. Maloney escreveu mais tarde uma nova versão de Morphic for Squeak, mas as idéias básicas por trás da versão Self ainda estão vivas e bem vivas no Pharo Morphic: directness e liveness. Directness significa que as formas na tela são objetos que podem ser examinados ou alterados diretamente, ou seja, clicando sobre eles usando um mouse. Liveness significa que a interface do usuário é sempre capaz de responder às ações do usuário: a informação na tela é continuamente atualizada conforme o mundo que ela descreve sofre mudanças. Um exemplo simples disto é que você pode destacar um item do menu e mantê-lo como um botão.

Abra o World Menu e clique Option-Command-Shift uma vez sobre ele para mostrar seus halo icons, depois repita a operação novamente em um item do menu que você deseja destacar, para mostrar aos halos daquele item (veja Figura 1-1).

Figura 1-1
Figura 1-2

Agora duplique esse item em outro lugar na tela agarrando o botão verde, como mostrado na Figura 1-1.

Uma vez feito o drop, o item do menu permanece destacado do menu e você pode interagir com ele como se estivesse no menu (veja Figura 1-2).

Este exemplo ilustra o que queremos dizer com “directness” e “liveness”. Isto dá muito poder ao desenvolver interfaces de usuário alternativas e prototipagem de interações alternativas.

Morphic mostra um pouco sua idade e a comunidade Pharo está trabalhando há vários anos em uma possível substituição. Substituir o Morphic significa ter uma nova infra-estrutura de baixo nível e um novo conjunto de widgets. O projeto é chamado Bloc e tem várias iterações. Bloc é sobre a infra-estrutura e Brick é um conjunto de widgets construídos no topo. Mas vamos nos divertir com o Morphic.

1.2 Morphs

Todos os objetos que você vê na tela quando abre o Pharo são Morphs, ou seja, são instâncias de subclasses da classe Morph. A própria classe Morph é uma classe grande com muitos métodos; isto torna possível que subclasses implementem comportamentos interessantes com pouco código.

Figure 1-4 (Morph new color: Color orange) openInWorld.

Para criar um morph para representar uma string, execute o seguinte código em um Playground.

Listagem 1-3 Criação de um String Morph

'Morph' asMorph openInWorld

Isto cria um Morph para representar a string ‘Morph’, e depois o abre (ou seja, o exibe) no world, que é o nome que o Pharo dá à tela. Você deve obter um elemento gráfico (um Morph), que pode ser manipulado por meio de um meta-clique.

É claro, é possível definir morphs que são representações gráficas mais interessantes do que aquela que você acabou de ver. O método asMorph tem uma implementação padrão na classe Object que apenas cria um StringMorph. Assim, por exemplo, Color tan asMorph retorna um StringMorph rotulado com o resultado de Color tan printString. Vamos mudar isto para que tenhamos um retângulo colorido em seu lugar.

Agora execute (Morph new colors: Color orange) openInWorld em um playground. Em vez do morph tipo string, você ontem um retângulo laranja (veja Figura 1-4)! Você obtém o mesmo executando (Morph new color: Color orange) openInWorld.

1.3 Manipulando morphs

Os Morphs são objetos, portanto podemos manipulá-los como qualquer outro objeto no Pharo: enviando mensagens, podemos mudar suas propriedades, criar novas subclasses de Morph, e assim por diante.

Cada morph, mesmo que atualmente não esteja aberto na tela, tem uma posição e um tamanho. Por conveniência, todos os morphs são considerados como ocupando uma região retangular da tela; se tiverem forma irregular, sua posição e tamanho são os da menor box retangular que os rodeia, que é conhecida como morph's bounding box, ou apenas seus bounds.

Figura 1-5 Bill e Joe após 10 movimentos.
  • O método position retorna um Point que descreve a localização do canto superior esquerdo do Morph (ou o canto superior esquerdo de sua bounding box).A origem do sistema de coordenadas é o canto superior esquerdo da tela, com as coordenadas y aumentando para baixo da tela e as coordenadas x aumentando para a direita.
  • O método extent também retorna um Point, mas este especifica a largura e a altura do Morph, em vez de uma localização.

Digite o seguinte código em um playground e Do it:

joe := Morph new color: Color blue. joe openInWorld.  
bill := Morph new color: Color red. bill openInWorld.

Em seguida, digite joe position e depois Print it.Para mover o joe, executar joe position: (joe position + (10@3)) repetidamente (ver Figura 1-5).

É possível fazer uma coisa semelhante com o size. joe extent retorna as dimensões de joe; para fazer joe “crescer” 10%, execute joe extent': (joe extent * 1.1). Para mudar a cor de um morph, envie a mensagem color: com o argumento da classe Color, por exemplo, joe color: Color orange. Para acrescentar transparência, tente joe color: (Cor orange alfa: 0.5).

Figura 1-6 Bill segue Joe.

Para fazer a bill seguir a joe, você pode executar repetidamente este código:

bill position: (joe position + (100@0))

Se você mover o joe usando o mouse e depois executar este código, o bill se moverá de modo que ele fique 100 pixels à direita do joe. Você pode ver o resultado na Figura 1-6. Nada de surpreendente.

Note que você pode apagar os morphs criados com o

  • enviando-lhes a mensagem delete
  • selecionando o ícone cross em halos que você pode abrir usando o clique Meta-Option.

1.4 Compondo morphs

Uma maneira de criar novas representações gráficas é colocando uma morph dentro de outro. Isto é chamado composição; os morphs podem ser compostos a qualquer profundidade. Você pode colocar um morph dentro de outro, enviando a mensagem addMorph: para o morph recipiente.

Tente adicionar um morph a outro da seguinte maneira:

balloon := BalloonMorph new color: Color yellow. 
joe addMorph: balloon.  
balloon position: joe position.

A última linha posiciona o balloon nas mesmas coordenadas que o joe. Observe que as coordenadas do morph contido ainda são relativas à tela, não ao morph que o contém. Esta forma absoluta de posicionar o morph não é realmente boa e faz com que a programação do morph pareça um pouco estranha. Mas há muitos métodos disponíveis para posicionar um morph; consulte o protocolo geometry da classe Morph para ver por si mesmo. Por exemplo, para centralizar o balão dentro do joe, execute balloon center: joe center.

Figura 1-7 O balloon está contido dentro do joe, o morph laranja translúcido.

Se você agora tentar agarrar o balloon com o mouse, verá que você realmente agarra o joe, e os dois morphs se movem juntos: o balloon está embutido dentro do joe. É possível embutir mais morphs dentro do joe. Além de fazer isso de forma programática, você também pode incorporar morphs por manipulação direta.

1.5 Criando e desenhando seus próprios morphs

Embora seja possível fazer muitas representações gráficas interessantes e úteis através da composição de morphs, às vezes será necessário criar algo completamente diferente.

Para fazer isso, você define uma subclasse de Morph e faz override do método drawOn: para mudar sua aparência.

O framework Morphic envia a mensagem drawOn: a um morph quando ele precisa reapresentar o morph na tela. O parâmetro para drawOn: é uma espécie de Canvas; o comportamento esperado é que o morph se desenhe sobre aquele canvas, dentro de seus próprios limites. Vamos utilizar este conhecimento para criar um morph em forma de cruz.

Usando o system browser, defina uma nova classe CrossMorph como subclasse de Morph:

Morph subclass: #CrossMorph 
    instanceVariableNames: '' 
    classVariableNames: '' 
    package: 'PBE-Morphic'

Podemos definir o método drawOn: assim:

CrossMorph >> drawOn: aCanvas  
    | crossHeight crossWidth horizontalBar verticalBar | 
    crossHeight := self height / 3.  
    crossWidth := self width / 3.  
    horizontalBar := self bounds insetBy: 0 @ crossHeight.
    verticalBar := self bounds insetBy: crossWidth @ 0. 
    aCanvas fillRectangle: horizontalBar color: self color. 
    aCanvas fillRectangle: verticalBar color: self color
Figura 1-8 A CrossMorph com seu halo; você pode redimensioná-lo como desejar.

O envio da mensagem bounds a um morph retorna a sua bounding box, que é uma instância de Rectangle. Os retângulos entendem muitas mensagens que criam outros retângulos com a geometria associada. Aqui, utilizamos a mensagem insetBy: com um Point como argumento para criar primeiro um retângulo com altura reduzida, e depois outro retângulo com largura reduzida.

Para testar seu novo morph, execute CrossMorph new openInWorld.

O resultado deve ser algo parecido com a Figura 1-8. No entanto, você notará que a zona sensível – onde você pode clicar para agarrar o morph – ainda é toda a bounding box. Vamos consertar isto.

Quando o framework Morphic precisa descobrir quais morphs se encontram sob o cursor, ele envia a mensagem containsPoint: a todos os morphs cujas bounding boxes se encontram sob o ponteiro do mouse. Portanto, para limitar a zona sensível do morph à forma de cruz, precisamos fazer override do método containsPoint:.

Defina o seguinte método na classe CrossMorph:

CrossMorph >> containsPoint: aPoint  
    | crossHeight crossWidth horizontalBar verticalBar | 
    crossHeight := self height / 3.  
    crossWidth := self width / 3.  
    horizontalBar := self bounds insetBy: 0 @ crossHeight.
    verticalBar := self bounds insetBy: crossWidth @ 0.  
    ^ (horizontalBar 
        containsPoint: aPoint) 
        or: [ verticalBar containsPoint: aPoint ]

Este método utiliza a mesma lógica do drawOn:, para que possamos estar seguros de que os pontos para os quais contenhaPoint: retorna true são os mesmos que serão coloridos pelo drawOn:. Observe como utilizamos o método containsPoint: na classe Rectangle para fazer o trabalho pesado.

Há dois problemas com o código nos dois métodos acima.

O mais óbvio é que temos o código duplicado.Isto é um erro elementar: se acharmos que precisamos mudar a forma como a horizontalBar ou verticalBar são calculadas, é bem provável que esqueçamos de mudar uma das duas ocorrências. A solução é considerar por estes cálculos em dois novos métodos, que colocamos no protocolo private:

CrossMorph >> horizontalBar  
    | crossHeight |  
    crossHeight := self height / 3.  
    ^ self bounds insetBy: 0 @ crossHeight
CrossMorph >> verticalBar  
    | crossWidth |  
    crossWidth := self width / 3.  
    ^ self bounds insetBy: crossWidth @ 0

Podemos então definir tanto drawOn: como containsPoint: utilizando estes métodos:

CrossMorph >> drawOn: aCanvas  
    aCanvas fillRectangle: self horizontalBar color: self color.
    aCanvas fillRectangle: self verticalBar color: self color
CrossMorph >> containsPoint: aPoint
    ^ (self horizontalBar containsPoint: aPoint) 
    or: [ self verticalBar containsPoint: aPoint ]

Este código é muito mais simples de entender, em grande parte porque demos nomes significativos aos métodos privados. Na verdade, é tão simples que talvez você tenha notado o segundo problema: a área no centro da cruz, que está sob as barras horizontais e verticais, é desenhada duas vezes. Isto não importa quando preenchemos a cruz com uma cor opaca, mas o bug se torna aparente imediatamente se desenharmos uma cruz semi-transparente, como mostrado na Figura 1-9.

Execute o seguinte código em um playground:

CrossMorph new 
    openInWorld; 
    bounds: (0@0 corner: 200@200); 
    color: (Color blue alpha: 0.4)
Figura 1-9 O centro da cruz é preenchido duas vezes com a cor.

A correção é dividir a barra vertical em três peças, e preencher somente a parte superior e inferior. Mais uma vez encontramos um método na classe Rectangle que faz o trabalho pesado para nós: r1 áreasOutside: r2 retorna um conjunto de retângulos que compreende as partes de r1 fora de r2. Aqui está o código revisado:

CrossMorph >> drawOn: aCanvas  
    | topAndBottom |  
    aCanvas fillRectangle: self horizontalBar color: self color.
    topAndBottom := self verticalBar 
        areasOutside: self horizontalBar. 
    topAndBottom do: [ :each | 
        aCanvas fillRectangle: each color: self color ]
Figura 1-10 O morph em forma de cruz, mostrando uma fileira de pixels não preenchidos.

Este código parece funcionar, mas se você o experimentar em algumas cruzes e redimensioná-las, você pode notar que em alguns tamanhos, uma linha de um pixel de largura separa o fundo da cruz do restante, como mostrado na Figura 1-10. Isto se deve ao arredondamento: quando o tamanho do retângulo a ser preenchido não é um inteiro, fillRectangle: color: parece arredondar de forma inconsistente, deixando uma fileira de pixels por preencher.

Podemos contornar isto arredondando explicitamente quando calculamos os tamanhos das barras, como mostrado a seguir:

CrossMorph >> horizontalBar  
    | crossHeight |  
    crossHeight := (self height / 3) rounded. 
    ^ self bounds insetBy: 0 @ crossHeight
CrossMorph >> verticalBar  
    | crossWidth |  
    crossWidth := (self width / 3) rounded. 
    ^ self bounds insetBy: crossWidth @ 0

1.6 Eventos do mouse para interação

Para construir interfaces de usuário “vivas” usando morphs, precisamos ser capazes de interagir com eles usando o mouse e o teclado. Além disso, os morphs precisam ser capazes de responder à entrada do usuário mudando sua aparência e posição – ou seja, animando-se a si mesmos.

Quando um botão do mouse é pressionado, Morphic envia a cada morph sob o ponteiro do mouse a mensagem handlesMouseDown:. Se um morph responde true, então Morphic envia imediatamente a mensagem mouseDown:; também envia a mensagem mouseUp: quando o usuário solta o botão do mouse. Se todos os morphs responderem com false, então Morphic inicia uma operação de arrastar e soltar. Como discutiremos abaixo, as mensagens mouseDown: e mouseUp: são enviadas com um argumento – um objeto MouseEvent – que codifica os detalhes da ação do mouse.

Vamos estender o CrossMorph para lidar com os eventos do mouse. Comecemos assegurando que todos os crossMorphs respondam com true à mensagem handlesMouseDown:. Adicione o método ao CrossMorph definido como a seguir:

CrossMorph >> handlesMouseDown: anEvent
    ^ true

Suponha que quando clicamos na cruz, queremos mudar a cor da cruz para vermelho, e quando clicamos com action-click sobre ela, queremos mudar a cor para amarelo. Definimos o método mouseDown: da seguinte forma:

CrossMorph >> mouseDown: anEvent
    anEvent redButtonPressed 
        ifTrue: [ 
            "click"
            self color: Color red 
        ].  
    anEvent yellowButtonPressed
        ifTrue: [ 
            "action-click"
            self color: Color yellow 
        ].  
    self changed

Observe que além de mudar a cor do morph, este método também envia a si mesmo self changed, o que garante que o framework Morphic envie drawOn: em tempo.

Note também que uma vez que o morph manipula os eventos do mouse, você não pode mais agarrá-lo com o mouse e movê-lo. Ao invés disso, você tem que utilizar o halo: o clique Option-Command-Shift na morph traz os halos, e pegue o brown move handle ou o black pickup handle na parte superior dos halos.

O argumento anEvent de mouseDown: é uma instância de MouseEvent, que é uma subclasse de MorphicEvent. O MouseEvent define os métodos RedButtonPressed e yellowButtonPressed. Navegue por esta classe para ver que outros métodos ela oferece para interrogar o evento do mouse.

1.7 Eventos do teclado

Para capturar os eventos do teclado, precisamos dar três passos.

  1. Dar o foco do teclado a um morph específico. Por exemplo, podemos dar foco ao nosso morph quando o mouse está sobre ele.
  2. Manusear o próprio evento do teclado com o método handleKeystroke:. Esta mensagem é enviada ao morph que tem o foco do teclado quando o usuário pressiona uma tecla.
  3. Remova o foco do teclado quando o mouse não estiver mais sobre nosso morph.

Vamos estender o CrossMorph para que ele reaja a toques de tecla. Primeiro, precisamos ser notificados quando o mouse estiver sobre o morph. Isto acontecerá se nosso morph responder com true à mensagem handlesMouseOver:.

CrossMorph >> handlesMouseOver: anEvent
    ^ true

Esta mensagem é o equivalente a handlesMouseDown: para a posição do mouse. Quando o ponteiro do mouse entra ou sai do morph, as mensagens mouseEnter: e mouseLeave: são enviadas a ele.

Defina dois métodos para que o CrossMorph capture e libere o foco do teclado, e um terceiro método para realmente lidar com os toques das teclas.

CrossMorph >> mouseEnter: anEvent
        anEvent hand newKeyboardFocus: self 
CrossMorph >> mouseLeave: anEvent
        anEvent hand newKeyboardFocus: nil 
CrossMorph >> handleKeystroke: anEvent 
    | keyValue |  
    keyValue := anEvent keyValue. 
    keyValue = 30 "up arrow"
        ifTrue: [self position: self position - (0 @ 1)]. 
    keyValue = 31 "down arrow"
        ifTrue: [self position: self position + (0 @ 1)]. 
    keyValue = 29 "right arrow"
        ifTrue: [self position: self position + (1 @ 0)].
    keyValue = 28 "left arrow"  
        ifTrue: [self position: self position - (1 @ 0)]

Escrevemos este método para que você possa mover o morph usando as setas do teclado. Note que quando o mouse não está mais sobre o morph, a mensagem handleKeystroke: não é enviada, então o morph deixa de responder aos comandos do teclado. Para descobrir os valores das teclas, você pode abrir uma janela do Transcript e adicionar Transcript show: anEvent keyValue ao método handleKeystroke:.

O argumento anEvent do handleKeystroke: é uma instância do KeyboardEvent, outra subclasse do MorphicEvent. Navegue por esta classe para aprender mais sobre eventos de teclado.

1.8 Morphic animations

Morphic fornece um sistema simples de animação com dois métodos principais: o “step” é enviado a um morph em intervalos regulares de tempo, enquanto o stepTime especifica o tempo em milissegundos entre os passos. O stepTime é na verdade o tempo mínimo entre as etapas. Se você requerer um stepTime de 1 ms, não se surpreenda se o Pharo estiver muito ocupado para enviar a mensagem step ao seu morph com tanta freqüência. Além disso, o startStepping liga o mecanismo da animação, enquanto o stopStepping o desliga novamente. O isStepping pode ser utilizado para descobrir se um morph está sendo animado no momento.

Faça o CrossMorph piscar, definindo estes métodos da seguinte forma:

CrossMorph >> stepTime
    ^ 100
CrossMorph >> step  
    (self color diff: Color black) < 0.1
        ifTrue: [ self color: Color red ]
        ifFalse: [ self color: self color darker ] 

Para começar, você pode abrir um inspetor em um CrossMorph utilizando o debug handle que parece uma ferramenta nos halos, digite startStepping no pequeno painel na parte inferior, e Do it.

Você também pode redefinir o método de initialize da seguinte forma:

CrossMorph >> initialize 
    super initialize.  
    self startStepping

Alternativamente, você pode modificar o método handleKeystroke: para que você possa utilizar as teclas + e – para iniciar e parar a animação. Adicione o seguinte código ao método handleKeystroke::

handleKeystroke: keyValue
    keyValue = $+ asciiValue  ifTrue: [ self startStepping ].
    keyValue = $- asciiValue ifTrue: [ self stopStepping ]

1.9  Interatores

Para solicitar informações ao usuário, a classe UIManager fornece um grande número de caixas de diálogo prontas para uso. Por exemplo, o método request:initialAnswer: retorna a string inserida pelo usuário (Figura 1-11).

UIManager default  
    request: 'What''s your name?' initialAnswer: 'no name'
Figura 1-11 Um diálogo de entrada.

Para exibir um menu popup, use um dos vários métodos chooseFrom: (Fig. 1-12):

UIManager default  
    chooseFrom: #(
        'circle' 
        'oval' 
        'square' 
        'rectangle' 
        'triangle') 
    lines: #(2 4) message: 'Choose a shape'
Figura 1-12 Pop-up menu.

Navegue na classe UIManager e experimente alguns dos métodos de interação oferecidos.

1.10 Drag-and-drop

O framework Morphic também suporta o drag-and-drop. Vamos examinar um exemplo simples com dois morphs, um receiver morph e um dropped morph.O receptor só aceitará um morph se o morph dropped corresponder a uma determinada condição: em nosso exemplo, o morph deve ser azul. Se ele for rejeitado, o morph dropped decide o que fazer.

Vamos primeiro definir o morph receptor:

Morph subclass: #ReceiverMorph 
    instanceVariableNames: '' 
    classVariableNames: '' 
    package: 'PBE-Morphic'

Agora defina o método de inicialização da maneira usual:

ReceiverMorph >> initialize  
    super initialize.  
    color := Color red.  
    bounds := 0 @ 0 extent: 200 @ 200

Como decidimos se o morph receptor aceitará ou rejeitará o dropped morph? Em geral, os dois morphs terão que concordar com a interação. O receptor faz isso respondendo a wantDroppedMorph:event:. Seu primeiro argumento é o dropped morph, e o segundo o evento do mouse, para que o receptor possa, por exemplo, ver se alguma tecla modificadora foi mantida pressionada no momento em que o dropped morph terminou de ser arrastado para cima dele. O dropped morph também tem a oportunidade de verificar e ver se gosta do morph no qual está sendo dropped, respondendo à mensagem wantsToBeDroppedInto:. O padrão de implementação deste método (na classe Morph) retorna true.

ReceiverMorph >> wantsDroppedMorph: aMorph event: anEvent 
    ^ aMorph color = Color blue

O que acontece com o dropped morph se o receiving morph não o quiser? O comportamento padrão é não fazer nada, ou seja, sentar-se em cima do receiving morph, mas sem interagir com ele. Um comportamento mais intuitivo é que o dropped morph volte à sua posição original. Isto pode ser alcançado pelo receiving morph retornando true à mensagem repelsMorph:event: quando ele não quer o dropped morph:

ReceiverMorph >> repelsMorph: aMorph event: anEvent
    ^ (self wantsDroppedMorph: aMorph event: anEvent) not

Isso é tudo o que precisamos no que diz respeito ao morph receptor.

Figura 1-13 A ReceiverMorph e um EllipseMorph.

Criar instâncias de ReceiverMorph e EllipseMorph em um playground:

ReceiverMorph new 
    openInWorld;  
    bounds: (100@100 corner: 200@200).
EllipseMorph new openInWorld.

Tente arrastar e soltar o ElipseMorph amarelo sobre o receptor. Ela será rejeitada e enviada de volta à sua posição inicial.

Para mudar este comportamento, mude a cor do ElipseMorph para a cor azul (enviando-lhe a mensagem color: Color blue; logo após o new). Morphs azuis devem ser aceitos pelo ReceptorMorph.

Vamos criar uma subclasse específica de Morph, chamada DroppedMorph, para que possamos experimentar um pouco mais. Vamos definir um novo tipo de morph chamado DroppedMorph.

Morph subclass: #DroppedMorph 
    instanceVariableNames: '' 
    classVariableNames: '' 
    package: 'PBE-Morphic'
DroppedMorph >> initialize
    super initialize.  
    color := Color blue. self position: 250 @ 100

Agora podemos especificar o que o dropped morph deve fazer quando é rejeitado pelo receptor; aqui ele ficará preso ao ponteiro do mouse:

DroppedMorph >> rejectDropMorphEvent: anEvent 
    |h|
    h := anEvent hand.  
    WorldState addDeferredUIMessage: [ h grabMorph: self ].
    anEvent wasHandled: true
Figura 1-14 Criação de DroppedMorph e ReceiverMorph.

Enviar a mensagem hand para um evento retorna o hand, uma instância de HandMorph que representa o ponteiro do mouse e o que quer que ele segure. Aqui dizemos ao World que a hand deve se prender a si mesma, o morph rejeitado.

Criar duas instâncias de DroppedMorph de cores diferentes, e depois arraste e solte-as sobre o receptor.

ReceiverMorph new openInWorld.  
morph := (DroppedMorph new color: Color blue) openInWorld. 
morph position: (morph position + (70@0)).  
(DroppedMorph new color: Color green) openInWorld.

O morph verde é rejeitado e, portanto, fica preso ao ponteiro do mouse.

1.11 Um exemplo completo

Vamos projetar um morph para rolar um dado. Clicando nele, serão exibidos os valores de todos os lados do dado em um rápido loop, e outro clique interromperá a animação.

Defina o dado como uma subclasse de BorderedMorph em vez de Morph, pois faremos uso da borda.

BorderedMorph subclass: #DieMorph 
    instanceVariableNames: 'faces dieValue isStopped'
    classVariableNames: ''  
    package: 'PBE-Morphic'
Figura 1-15 O dado em Morphic.
DieMorph class >> faces: aNumber
    ^ self new faces: aNumber

O método de initialize é definido no instance side da maneira usual; lembre-se que new envia automaticamente initialize para a instância recém-criada.

DieMorph >> initialize 
    super initialize. 
    self extent: 50 @ 50. 
    self useGradientFill; borderWidth: 2; 
    useRoundedCorners.
    self setBorderStyle: #complexRaised. 
    self fillStyle direction: self extent. self color: Color green.  
    dieValue := 1.
faces := 6. isStopped := false

N.T: Lembre-se de incluir a mensagem super initialize no initialize para que as superclasses possam fazer as suas inicializações. Se esquecer vai obter comprotamentos estranhos por falta de inicialização de algumas variáveis nas superclasses.

Utilizamos alguns métodos de BorderedMorph para dar uma aparência agradável ao dado: uma borda espessa com elevações, cantos arredondados e um gradiente de cor na face visível. Definimos o método de instância faces: para verificar um parâmetro válido, como segue:

DieMorph >> faces: aNumber 
    "Set the number of faces"
    ((aNumber isInteger and: [ aNumber > 0 ]) 
        and: [ aNumber <= 9 ]) ifTrue: [ faces := aNumber ]

Pode ser bom rever a ordem na qual as mensagens são enviadas quando um dado é criado. Por exemplo, se começarmos por avaliar DieMorph faces: 9:

  • O método de classe classe DieMorph >> faces: envia novo para classe DieMorph.
  • O método para new (herdado pela classe DieMorph de Behavior) cria a nova instância e lhe envia a mensagem inicializar.
  • O método de inicializar' em DieMorph’ define faces com um valor inicial de 6.
  • A classe DieMorph >> new' retorna ao método de classe DieMorph class >> faces:“, que então envia as faces da mensagem: 9 para a nova instância.
  • O método de instância DieMorph >> faces: agora executa, configurando a variável de instância faces para 9.

Antes de definir drawOn:, precisamos de alguns métodos para colocar os pontos sobre a face exibida:

DieMorph >> face1 
    ^ {(0.5 @ 0.5)}
DieMorph >> face2 
    ^{0.25@0.25 . 0.75@0.75}
DieMorph >> face3  
    ^{0.25@0.25 . 0.75@0.75 . 0.5@0.5}
DieMorph >> face4  
    ^{0.25@0.25 . 0.75@0.25 . 0.75@0.75 . 0.25@0.75}
DieMorph >> face5  
    ^{0.25@0.25 . 0.75@0.25 . 0.75@0.75 . 0.25@0.75 . 0.5@0.5}
DieMorph >> face6  
    ^{0.25@0.25 . 0.75@0.25 . 0.75@0.75 . 
        0.25@0.75 . 0.25@0.5 .
        0.75@0.5}
    
DieMorph >> face7  
    ^{0.25@0.25 . 0.75@0.25 . 0.75@0.75 . 
        0.25@0.75 . 0.25@0.5 .
    
        0.75@0.5 . 0.5@0.5}
DieMorph >> face8  
    ^{0.25@0.25 . 0.75@0.25 . 0.75@0.75 . 
        0.25@0.75 . 0.25@0.5 .
        0.75@0.5 . 0.5@0.5 . 0.5@0.25}
    DieMorph >> face9  
    ^{0.25@0.25 . 0.75@0.25 . 0.75@0.75 . 
    0.25@0.75 . 0.25@0.5 .
    0.75@0.5 . 0.5@0.5 . 0.5@0.25 . 0.5@0.75}

Listing 1-16 Create a Die 6

(DieMorph faces: 6) openInWorld.

Estes métodos definem coleções das coordenadas dos pontos para cada face. As coordenadas estão em um quadrado de tamanho 1×1; simplesmente precisaremos mudar a escala para um dimensionamento adequado.

Figura 1-17 Um novo molde 6 com (faces DieMorph: 6) openInWorld

O método drawOn: faz duas coisas: desenha o fundo do dado com o super drawOn:, e depois desenha os pontos da seguinte forma:

DieMorph >> drawOn: aCanvas  
    super drawOn: aCanvas.  
    (self perform: ('face', dieValue asString) asSymbol) 
        do: [ :aPoint | self drawDotOn: aCanvas at: aPoint ]

A segunda parte deste método utiliza as capacidades de reflexão do Pharo. O desenho dos pontos de uma face é uma simples questão de iteração sobre a coleção dada pelo método faceX para aquela face, enviando a mensagem drawDotOn:at: para cada coordenada. Para chamar o método faceX correto, utilizamos o método perform: que envia uma mensagem construída a partir de uma string, ('face', dieValue asString) asSymbol.

DieMorph >> drawDotOn: aCanvas at: aPoint
    aCanvas fillOval: (
        Rectangle 
            center: self position + (self extent * aPoint)
            extent: self extent / 6) 
            color: Color black

Como as coordenadas são normalizadas para o intervalo [0:1], nós as escalamos para as dimensões de nosso dado: self extent * aPoint. Já podemos criar uma instância do dado a partir de um playground (ver resultado na Figura 1-17):

Para mudar a face exibida, criamos um acessor que podemos usar assim myDie dieValue: 5:

DieMorph >> dieValue: aNumber  
    ((aNumber isInteger 
    and: [ aNumber > 0 ]) 
    and: [ aNumber <= faces]) 
        ifTrue: [ dieValue := aNumber. self changed ]
Figura 1-18 Resultado de (DieMorph faces: 6) openInWorld; dieValue: 5.

Agora vamos usar o sistema de animação para mostrar rapidamente todas as faces:

DieMorph >> stepTime
         ^ 100 

 DieMorph >> step
         isStopped ifFalse: [self dieValue: (1 to: faces) atRandom]

Agora o dado está rolando!

Para iniciar ou parar a animação clicando, usaremos o que aprendemos antes dos eventos do mouse. Primeiro, ative a recepção dos eventos do mouse:

DieMorph >> handlesMouseDown: anEvent
    ^ true

Em segundo lugar, vamos parar e começar, alternativamente, com um clique do mouse.

DieMorph >> mouseDown: anEvent
    anEvent redButtonPressed 
        ifTrue: [isStopped := isStopped not]

Agora o dado vai rolar ou parar de rolar quando clicarmos sobre ele.

1.12 Mais sobre Canvas

O método drawOn: tem como único argumento uma instância de Canvas; o canvas é a área sobre a qual o morph se desenha. Usando os métodos gráficos do canvas, você está livre para dar a aparência que deseja a um morph. Se você navegar na hierarquia de herança da classe Canvas, verá que ela tem várias variantes. A variante padrão do Canvas é FormCanvas, e você encontrará os principais métodos gráficos em Canvas e FormCanvas. Estes métodos podem desenhar pontos, linhas, polígonos, retângulos, elipses, textos e imagens com rotação e escalas.

Também é possível utilizar outros tipos de canvas. Para utilizar estes recursos, você precisará de uma AlphaBlendingCanvas ou um BalloonCanvas. Mas como você pode obter tal canvas? Felizmente, você pode transformar um tipo de canvas em outro.

Para usar um canvas com uma transparência 0,5 alfa no DieMorph, redefina o drawOn: desta forma:

DieMorph >> drawOn: aCanvas  
    | theCanvas |  
    theCanvas := aCanvas asAlphaBlendingCanvas: 0.5. 
    super drawOn: theCanvas.  
    (self perform: ('face', dieValue asString) asSymbol) 
        do: [:aPoint | self drawDotOn: theCanvas at: aPoint]
Figura 1-19 O dado apresentado usando transparência

Isso é tudo o que você precisa fazer!

1.13 Chapter summary

Morphic é uma estrutura gráfica na qual elementos de interface gráfica podem ser compostos dinamicamente.

  • Você pode converter um objeto em um morph e exibir esse morph na tela enviando-lhe as mensagens asMorph openInWorld.
  • Você pode manipular um morph usando o clique Command-Option+Shift sobre ele e usando os halos que aparecem. (Os ícones têm balões de ajuda que explicam o que eles fazem).
  • Você pode compor morphs incorporando um no outro, seja arrastando e soltando ou enviando a mensagem addMorph:.
  • Você pode subclassificar uma classe de morph existente e redefinir métodos chave, tais como initialize e drawOn:.
  • Você pode controlar como um morph reage a eventos do mouse e teclado redefinindo os métodos handlesMouseDown:handlesMouseOver:, etc.
  • Você pode animar um morph definindo os métodos step (o que fazer) e stepTime (o número de milissegundos entre os steps).

3 comentários

Deixe um comentário