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).
![](https://chicoary.wordpress.com/wp-content/uploads/2021/09/pasted-image-20210914143732.png?w=340)
![](https://chicoary.wordpress.com/wp-content/uploads/2021/09/pasted-image-20210914144233.png?w=251)
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.
![](https://chicoary.wordpress.com/wp-content/uploads/2021/09/pasted-image-20210914145134.png?w=886)
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
.
![](https://chicoary.wordpress.com/wp-content/uploads/2021/09/pasted-image-20210914184337.png?w=654)
Bill
e Joe
após 10 movimentos.- O método
position
retorna umPoint
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 coordenadasy
aumentando para baixo da tela e as coordenadasx
aumentando para a direita. - O método
extent
também retorna umPoint
, 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)
.
![](https://chicoary.wordpress.com/wp-content/uploads/2021/09/pasted-image-20210914190324.png?w=669)
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
.
![](https://chicoary.wordpress.com/wp-content/uploads/2021/09/pasted-image-20210914191942.png?w=202)
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
![](https://chicoary.wordpress.com/wp-content/uploads/2021/09/pasted-image-20210914193237.png?w=354)
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)
![](https://chicoary.wordpress.com/wp-content/uploads/2021/09/pasted-image-20210914195454.png?w=575)
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 ]
![](https://chicoary.wordpress.com/wp-content/uploads/2021/09/pasted-image-20210914200343.png?w=528)
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.
- Dar o foco do teclado a um morph específico. Por exemplo, podemos dar foco ao nosso morph quando o mouse está sobre ele.
- 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. - 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'
![](https://chicoary.wordpress.com/wp-content/uploads/2021/09/pasted-image-20210926081114.png?w=618)
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'
![](https://chicoary.wordpress.com/wp-content/uploads/2021/09/screen-shot-2021-09-26-at-08.14.16.png?w=620)
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.
![](https://chicoary.wordpress.com/wp-content/uploads/2021/09/pasted-image-20210926085616.png?w=923)
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
![](https://chicoary.wordpress.com/wp-content/uploads/2021/09/pasted-image-20210926090902.png?w=650)
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'
![](https://chicoary.wordpress.com/wp-content/uploads/2021/09/pasted-image-20210926091922.png?w=275)
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
noinitialize
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:
envianovo
paraclasse DieMorph
. - O método para
new
(herdado pela classeDieMorph
deBehavior
) cria a nova instância e lhe envia a mensageminicializar
. - 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ânciafaces
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.
![](https://chicoary.wordpress.com/wp-content/uploads/2021/09/pasted-image-20210926092941.png?w=208)
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 ]
![](https://chicoary.wordpress.com/wp-content/uploads/2021/09/pasted-image-20210926093352.png?w=280)
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]
![](https://chicoary.wordpress.com/wp-content/uploads/2021/09/pasted-image-20210926093837.png?w=287)
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
edrawOn:
. - 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) estepTime
(o número de milissegundos entre os steps).
[…] Reler o tópico Morphic no Pharo Smalltalk. […]
[…] Use os halos para movimentar os morphs (Veja como em Morphic no Pharo Smalltalk). […]
[…] o tópico Morphic no Pharo Smalltalk no subtópico 1.8 Morphic […]