Tracer une ligne

RocouRocou Membre
avril 2022 modifié dans API AppKit #1

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?

Mots clés:

Réponses

  • Ton IBOutlet ne devrait-il pas être connecté au NSViewController 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 le drawRect:, 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 du drawRect:

  • 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 ?

  • @Larme a dit :
    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 :

    @Invalidating(.display) var alternateState: Bool = false 
    

    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 😉

  • LarmeLarme Membre
    avril 2022 modifié #10

    @Rocou a dit :
    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.

    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.

  • @Rocou, un pararallèle avec ObjC qui pourra peut-être t'inspirer : pour lier une IBAction d'un contrôleur à l'interface, il faut qu'une instance de ce contrôleur soit présente dans le fichier xib... mais je ne saurais dire comment traduire ça en Swift... ;)
  • @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")
    }

  • @Rocou a dit :
    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.

    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.

    Attention: en Swift une méthode doit être marquée @objc ou @IBAction pour être utilisée comme un sélecteur .

    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 :

    let button = NSButton(title: "Toggle", target: self, action: #selector(toggleState))
    
    • target : l'objet cible
    • action : 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) :

    if let target = self.target, let action = self.action {
        self.action.perform(selector, with: self)
    }
    

    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 :

    • nom: à ta convenance
    • interface: storyboard
    • pas de Core Data
    • pas de test

    Crée un fichier Swift vide DrawOnView.swift ajoute-y import Cocoa et colle-y la classe DrawOnView depuis le gist.
    Crée une nouvelle Cocoa Class, nomme la DrawOnViewController, à Subclass of: mets NSViewController et décoche Also create XIB file.... Tu as un nouveau fichier DrawOnViewController.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:

    @IBOutlet weak var drawOnView: DrawOnView!
    

    Il doit aussi recevoir un message du bouton alors il lui faut une méthode notée @IBAction. Ajoute lui la méthode correspondante :

    @IBAction func toggleDrawOnView(_ sender: Any?) {
        drawOnView.alternateState.toggle()
    }
    

    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 mets Main.storyboard et DrawOnViewController.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 en DrawOnViewController. 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?

  • @Rocou a dit :
    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.

  • RocouRocou Membre
    avril 2022 modifié #18

    Ç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 😉

Connectez-vous ou Inscrivez-vous pour répondre.