Tracer une ligne
Rocou
Membre
Bonjour,
Je suis sûr que mon problème est tout con mais je n'y arrive pas.
J'aimerais tracer un segment dans une vue. (Sur Mac).
J'ai donc créé une classe et tout fonctionne à merveille:
class DrawOnView: NSView {
override func draw(_ rect: CGRect) {
let context = NSGraphicsContext.current?.cgContext
context?.setLineWidth(2.0)
context?.setStrokeColor(NSColor.blue.cgColor)
context?.move(to: CGPoint(x:0, y: 0))
context?.addLine(to: CGPoint(x: 20, y: 30))
context?.strokePath()
}
}
J'ai un petit segment bleu qui est tracé dans ma vue.
Ce que j'aimerais c'est en cliquant sur un bouton, effacer ma vue et tracer un autre segment dans celle-ci.
J'ai donc créé un bouton et associé une IBAction à celui-ci mais je ne sais pas comment indiquer que c'est dans la vue précédemment décrite que je veux dessiner. J'imagine qu'il faut passer l'identifiant de la vue à l'action mais comment?
Connectez-vous ou Inscrivez-vous pour répondre.
Réponses
Ton
IBOutlet
ne devrait-il pas être connecté auNSViewController
qui a cette vue, et lorsque tu veux redessiner, tu appelles dans ce dernier[theView setNeedsDisplay]
, où je ne sais plus quelle méthode qui va rappeler ledrawRect:
, donc pour voir si ça fonctionne, j'y mettrais un bool, qui en fonction de ce dernier changerait les valeurs des points (pour être sûr que ça redraw bien), et togglerait ce bool à la fin dudrawRect:
Je te remercie pour tes pistes.
En pratique, l'absence d'exemple de code pour MacOS complique singulièrement mon développement
setNeedsDisplay est dépréciée dans AppKit et je ne sais absolument pas par quoi remplacer cette méthode.
La doc ne l'indique pas
Force un display alors ?
Mais de quelle façon?
display() ne se compile pas ("Cannot find 'display' in scope")
Bonjour à tous,
@Rocou : Par sûr que ça aide mais en Obj-C, la syntaxe est : "instanceView.needsDisplay=YES"...
Merci pour ton aide @MortyMars mais effectivement cela ne fonctionne pas.
J'avais fait des trucs sympas en objective-c mais j'ai beaucoup de mal à convertir ce code en swift.
Un bon gist valant mieux qu'un long discours : regarde donc ça
Tu peux passer la partie preview. C'est juste pour pouvoir tester ça comme une vue SwiftUI. Colle le code dans un nouveau fichier de ton projet et c'est parti !
Mais en gros il faut utiliser
needsDisplay = true
pour demander à une vue de se redessiner.Depuis peu il y a le property wrapper
@Invalidating
qui permet de le faire plus facilement.Pour l'utiliser tu remplace les lignes 27 à 29 avec :
Mais j'ai vu des gens s'en plaindre, ça marcherait pas si bien que ça. Je ne l'utilise pas personnellement.
Sinon le code est plutôt simple ça devrait aller 😉
J'viens de remarquer que tu avais écrit en Swift et que je répondais en Objective-C... Deuxième fois que ça m'arrive cette semaine d'inverser l'un et l'autre... >_<
Merci @Pyroh
Je suppose que je dois lier mon bouton à @IBAction func toggleState(_ sender: Any?) mais je n'y arrive pas.
l'IBAction n'est pas dans le bon contrôleur.
@MortyMars Si j'ai bien compris, cette instance se traduit comme ceci en Swift (cf code de @Pyroh ):
var drawOnView: DrawOnView
Le souci est que ça ne se compile pas. Le compile l'indique soudainement que ma classe n'a pas de "initializers"
Si je reprends le code de @Pyroh dans la classe alors la compilation plante et je ne comprends pas ce que cela signifie.
Elle plante sur cette ligne en affichant le message: "has not been implemented"
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
Je vais essayer de faire simple parce que la matière est compliquée et que je ne suis pas forcément le meilleur des pédagogues (même si j'ai enseigné). Pour les connaisseurs, je sais que c'est plus compliqué que ça 😉
Il faut commencer par la base qui est le mécanisme d'envoi de messages entre objets. Objective-C est basé sur Smalltalk-80 (ça veut dire discussion en anglais) qui a lancé l'idée d'objets communiquant entre eux. Simplement, on a des objets —instances de
NSObject
ici— qui vont s'envoyer des messages qui comportent un sélecteur (un nom de méthode) et un ou plusieurs arguments optionnels. Pour les objets Swift (non-NSObject
) c'est différent mais on s'en fout un peu.Envoyer des messages
Pour envoyer un message à un objet il faut avoir une référence à l'objet en question, un sélecteur pour la méthode à invoquer et ensuite utiliser la méthode
func perform(_ aSelector: Selector!) -> Unmanaged<AnyObject>!
(prendre les variantes avec plus d'argument si il faut envoyer des arguments, regarde la doc).Dans notre cas quand tu connecte l'action du bouton tu lui donne ces deux infos : le sélecteur et l'objet auquel demander d'exécuter la fonction correspondante au-dit sélecteur. Et c'est la ligne 82 du gist qui le fait :
target
: l'objet cibleaction
: le sélecteur pour la méthode à appeler.Quand on clique sur le bouton il va simplement faire quelque chose dans ce goût-là (en vrai non, mais on dit) :
Et dans les Nib, Xib, Storyboards ?
C'est là qu'entre en jeu
@IBAction
. Ça sert à dire à Interface Builder (le fameux IB) que cette méthode est utilisable comme comme action pour un contrôle, un bouton par exemple. Ensuite Xcode fait sa soupe et en extrait un sélecteur qu'il va passer au contrôle. On a alors le message mais il manque toujours le correspondant à qui il va falloir envoyer la-dite missive.Et c'est là que tu es coincé. Le coupable c'est toi en premier mais Interface Builder arrive en bon second. IB ne va te permettre de lier l'action d'un contrôle et un
@IBAction
qui si —et seulement si— il parvient à trouver un lien logique entre le contrôle et la classe de laquelle est extrait l'@IBAction
. Cas complexe à part il faut que le contrôle soit un parent (même indirect) d'un objet de la même classe que celle dont est extrait l'@IBAction
. Ouais, dur.Un exemple ?
Oui.
Mise en place
Crée un nouveau projet macOS de type App :
Crée un fichier Swift vide
DrawOnView.swift
ajoute-yimport Cocoa
et colle-y la classeDrawOnView
depuis le gist.Crée une nouvelle Cocoa Class, nomme la
DrawOnViewController
, à Subclass of: metsNSViewController
et décoche Also create XIB file.... Tu as un nouveau fichierDrawOnViewController.swift
Layout
C'est parti pour l'interface, ouvre
Main.storyboard
. Dans la vue ajoute un bouton et change le titre pour Toggle (au hasard) puis en-dessous ajoute une Custom View. Tu obtiens ça :Maintenant sélectionne la Custom View et change sa classe pour
DrawOnView
dans l'Identity Inspector.Sélectionne maintenant les deux vues et arrange les dans une Stack View.
Sélectionne la vue qui s'appelle maintenant
DrawOnView
et contrains-la à une taille de 140 x 140.Prends la Stack View et change sa Hugging Priority pour 750 aussi bien en vertical qu'en horizontal. Et contrains-la ensuite comme suit :
Pour la partie layout on a fini, tu devrais obtenir ça:
Le vif du sujet maintenant !!
IBTruc et IBChose
Réfléchissons quelques secondes maintenant et voyons ce qu'on sait:
Il faut qu'on dise au bouton de dire à un object mystère de dire à la vue de changer une de ses propriétés et ensuite la vue doit se redessiner. Tout ça à chaque fois que le bouton est cliqué.
La partie vue on l'a déjà codé, c'est bon.
L'objet mystère on a qu'à dire qu'il va être de la classe
DrawOnViewController
. Alors on va modifier le fichier correspondant. Il va devoir manipuler la vue et lui demander de changer une propriété. Pour qu'il ait une référence à la vue il faut déclarer un@IBOutlet
qu'on connectera ensuite dans IB.Ajoute cette propriété à la classe
DrawOnViewController
:Il doit aussi recevoir un message du bouton alors il lui faut une méthode notée
@IBAction
. Ajoute lui la méthode correspondante :Et on retourne dans le storyboard pour tout connecter !
Le soucis c'est que pour le moment
DrawOnViewController
n'a rien à voir avec notre storyboard. On a bien un View Controller et un First Responder mais c'est tout. Donc si tu metsMain.storyboard
etDrawOnViewController.swift
côte à côte les connexions ne vont pas se faire. Jamais.Parce que dans la hiérarchie de ta vue il n'y a aucun objet de type
DrawOnViewController
et IB ne peut pas en déduire un lien logique. En plus il a raison, y'en a pas. Conceptuellement y'en a un mais dans notre tête et Xcode n'y est pas (encore heureux...).Bref, la règle de base est la suivante : il y a un seul view controller par scène, il porte une référence aux contrôles dont il a besoin et centralise les actions en offrant autant d'
@IBAction
que nécessaire. Ensuite il se débrouille gère les inputs.La scène en question c'est ça :
Par facilité on dira que tout ce qui compose cette scène sont des contrôles. Le contrôleur c'est le carré blanc dans un disque bleu, sélectionne le. Sa classe c'est
ViewController
, on va changer ça enDrawOnViewController
. Maintenant la classe est dans la hiérarchie et IB devrait retrouver ses petits.Effectivement, maintenant ça
marche !
Alors exécute le projet sans plus attendre et vois ce qu'on a accompli !
Conclusion
J'espère avoir réussi à être compréhensible. Que maintenant tu comprends que les objets doivent avoir un lien à travers la hiérarchie pour pouvoir leur envoyer des actions. J'ai pas trop développé l'aspect
@IBOutlet
mais je pense que tu connais.Après je me répète mais la réalité est plus complexe que ça et l'envoi de message entre objets bien plus puissant quand on considère la responder chain mais ça sera pour la prochaine fois 🙃
Merci beaucoup @Pyroh pour ces explications très détaillées!
Pour l'instant, je les ai lues et je pense savoir comme faire pour reproduire cela, sauf ce passage:
"Sélectionne maintenant les deux vues et arrange les dans une Stack View."
Quelle est donc cette seconde vue?
Puis vient une sorte de menu afin de "arrange les dans une Stack View" Comment fais-tu apparaitre ce menu?
Mais de rien !
Les vues en question c'est la Custom View et le bouton.
Et le menu apparait quand tu appuie sur le bouton que tu vas en dessous à gauche du menu sur le screen shot (le rectangle avec la flèche qui rentre dedans).
La Stack View est-elle nécessaire? Parce que je n'arrive jamais à lui donner l'aspect que je veux. Toute mon organisation est cassée, les objet s'affichent n'importe où, les uns sur les autres ou en dehors de ma vue principale. J'ai bien entendu essayé des tas de combinaisons de contraintes. Je n'ai pas compris le mécanisme.
Ça y est, réussi! Merci @Pyroh, tes explications ont eu le mérite de tout me faire tout reprendre depuis le début. J'ai été un peu perdu sur le paragraphe dédié à la Stack View qui restera, je le crains, un mystère pour moi.
Au final, je savais que c'était un détail tout con qui me manquait, en l'occurrence un IBOutlet lié à la vue dans laquelle je veux dessiner...
Oui elle est nécessaire, c'est le Cocoa moderne. C'est beaucoup plus simple à utiliser que d'arranger toi-même les vues avec des contraintes ou pire faire de la frame calculation. Là pour le coup j'ai pas mieux à te dire que de lire la doc. Et d'insister sur le principe de hugging et compression resistance qui font tout le sel des stack views.
Normalement si tu suis mes instructions ça fonctionne pour l'exemple donné. J'ai fait et immédiatement écrit ce que j'avais fait. Mais autolayout est clairement mauvais par rapport à ce que SwiftUI propose. Mais tellement plus pratique et rapide que de se taper les frames à la main.
Une fois que t'as compris le truc avec les stack views ça va (presque) tout seul. Mais encore de nos jours devenir forgeron nécessite de forger, beaucoup forger et forger encore 😉