Interagir avec un objet Cocoa par la programmation

Bonjour à tous,

Débutant sous Cocoa, je coince sur un problème d'accès aux objets d'une interface pourtant minimaliste...
Le contexte tient au fonctionnement d'une fenêtre de base incluant un NSTextField et un NSButton.
Après avoir créé les connections utiles IBOutlet et IBAction liant le code et les objets, je parviens à modifier le texte de mon NSTextField par un clic sur le NSButton ; mais ce que je souhaiterais faire en réalité c'est réaliser cette modification par programmation, en faisant appel à une méthode d'instance.
J'imagine que la problématique tient à envoyer le message (méthode d'instance) à l'objet NSTextField de la fenêtre affichée à l'écran, sur lequel je ne dispose d'aucun pointeur : c'est ce que j'aimerais savoir faire...

Merci de votre aide ;-)

déclaration : la classe MCNconnecteur est créée pour faire office de contrôleur
l'IBOutlet *texteChampTexte pointe sur le NSTextField
et l'IBAction mettreAjourParClic pointe sur le NSButton

implémentation

appel de la méthode

Réponses

  • @devulder a dit :
    newMCNconnecteur.texteChamp.stringValue = @"mon texte"; devait faire l'affaire!

    Ca fait bizarre de revoir du code Objc étant passer sous Swift ;)

    Merci pour le retour rapide :)

    J'ai modifié le code de AppDelegate selon ton conseil, mais pas plus de succès.
    N'est-ce pas comme je le pense un souci d'adressage de message à la bonne instance de fenêtre ou/et de NSTextField ?

    PS : Pour tout dire je tente de décortiquer et de reprendre le code d'un jeu d'échecs écrit en Objc, et comme je serais bien incapable de le traduire en qqchose de plus récent....
    Mais je retiens la remarque taquine et sans doute motivée :) , pour un prochain projet.
    En deux mots les avantages de Swift par rapport à Objc ?

    Bien cordialement ;)

  • CéroceCéroce Membre, Modérateur

    Mon hypothèse est que la problème vient de l'endroit où tu instancies le MCNConnecteur.
    Pour avoir une IBOutlet sur le textField, tu as dû être obligé d'ajouter une instance de MCNConnecteur dans ton xib/storyboard. Me trompe-je ?

    Alors que quand le MCNConnecteur est instancié par l'AppDelegate, à aucun moment l'outlet texteChampTexte n'est fixée. Si tu regardes au débogueur, sa valeur doit être nil (0x0000000).

    Il y a un autre problème. Quand tu instancies ton MCNConnecteur dans -[applicationDidFinishLaunching:], l'instance va être désallouée dès que le code sort de la méthode; forcément puisque c'est une variable locale. Tu dois la conserver en variable d'instance en déclarant une property.

  • Merci Céroce pour ta réponse et l'intérêt que tu portes à mon problème :)

    @Céroce a dit :
    Mon hypothèse est que la problème vient de l'endroit où tu instancies le MCNConnecteur.

    Mis à part dans AppDelegate (qui présentait de mon point de vue de débutant, l'avantage d'être appelée à coup sûr par le programme), où verrais-tu avantageusement cette instanciation ?

    Pour avoir une IBOutlet sur le textField, tu as dû être obligé d'ajouter une instance de MCNConnecteur dans ton xib/storyboard. Me trompe-je ?

    Oui, c'est bien ce que j'ai fait ;)

    Alors que quand le MCNConnecteur est instancié par l'AppDelegate, à aucun moment l'outlet texteChampTexte n'est fixée. Si tu regardes au débogueur, sa valeur doit être nil (0x0000000).

    J'avoue ne pas savoir comment faire pour vérifier cette valeur.
    J'ai bien posé un break point dans -[applicationDidFinishLaunching:] mais texteChampTexte n'est pas vraiment bavard...

    Il y a un autre problème. Quand tu instancies ton MCNConnecteur dans -[applicationDidFinishLaunching:], l'instance va être désallouée dès que le code sort de la méthode; forcément puisque c'est une variable locale. Tu dois la conserver en variable d'instance en déclarant une property.

    Je crois comprendre l'idée de ce que tu diagnostiques là :) mais quant à le mettre en application... :/
    J'ai tenté un "@property MCNconnecteur *monMCNConnecteur;" mais c'est peut-être une énorme co...rie !?
    Accepté en compilation mais pas efficace...

    Merci du temps que tu pourrais encore consacrer au noob que je suis :/

    Bien cordialement ;)

  • Bonjour à tous,

    Des nouvelles, pour tous ceux que l'exercice pourrait également concerner...
    À force de tâtonnements j'ai fini par résoudre le problème, et l'application-test fonctionne désormais comme attendu.
    Le souci (trop bête, my bad) était dû à une mauvaise insertion dans le code des connexions avec l'interface.
    Je retiendrai pour l'avenir à titre tout à fait personnel, qu'une organisation fonctionnelle doit répondre à deux principes simples :
    1. Rassembler toutes les connexions avec l'interface (IBOutlet /IBAction) dans une classe unique ayant vocation de contrôleur
    2. Positionner un IBOutlet d'une instance de cette 'classe contrôleur' dans chaque entête de classe ayant besoin d'un accès aux objets de la vue

    Le code corrigé et nettoyé de l'appli est en PJ pour info, sachant que l'interface a cette tête :

    Bien cordialement

  • CéroceCéroce Membre, Modérateur
    novembre 2020 modifié #6

    Comme tu as l'air d'avoir du background avec d'autres langages, je pense que le concept du MVC ne t'es pas étranger.

    Ce qui faut comprendre est que les outlets et les actions se tirent normalement vers un contrôleur; typiquement un NSWindowController (ou un NSViewController si tu veux découper ta fenêtres en sous-vues, si elles doivent être réutilisées ou que la fenêtre est complexe).
    Celui-ci peut être instancié dans le xib ou storyboard.

    Pour le modèle, tu n'en n'as pas défini dans ton exemple. Ça peut être un simple héritier de NSObject.
    Ça amène une question: où instancier l'objet racine du Modèle ?

    • Si l'application utilise des documents, ce sera dans une sous-classe de NSDocumentController.
    • Si l'application n'utilise pas de documents (un seul stockage pour toutes les données), alors généralement, l'objet racine sera instancié par l'AppDelegate.

    Dans les deux cas, il faudra déclarer une propriété pour conserver l'objet.

  • CéroceCéroce Membre, Modérateur

    @MortyMars a dit :

    @Céroce a dit :
    Mon hypothèse est que la problème vient de l'endroit où tu instancies le MCNConnecteur.

    Mis à part dans AppDelegate (qui présentait de mon point de vue de débutant, l'avantage d'être appelée à coup sûr par le programme), où verrais-tu avantageusement cette instanciation ?

    En théorie, un NSObject peut faire office de connecteur, mais en pratique, il faut utiliser un NSWindowController.
    Il faut comprendre un minimum comment ça marche: un objet va instancier un NSWindowController qui va charger un xib/storyboard. Ce fichier dit: je comporte tels objets avec telles propriétés. J'ai des outlets qui pointent vers File's Owner (ou vers un objet interne au xib/storyboard).
    Qui est File's Owner ? L'objet qui instancie le xib/storyboard, donc le NSWindowController.
    Ainsi, disons que le NSTextField est instancié à partir du xib, et que l'outlet est appelée nameTextField, ça va copier la valeur du pointeur sur le NSTextField dans la propriété nameTextField du NSWindowController.

    Ça utilise un mécanisme qu'on appelle le «Key-Value coding», qui permet de fixer une propriété dynamiquement en utilisant son nom. D'ailleurs tu vas bientôt faire une erreur à mélanger les outlets, et tu auras un message dans la console du type «Key error: Undefined key 'nameTextField'»

    Pour avoir une IBOutlet sur le textField, tu as dû être obligé d'ajouter une instance de MCNConnecteur dans ton xib/storyboard. Me trompe-je ?

    Oui, c'est bien ce que j'ai fait ;)

    Alors tu avais deux instances différentes de MCNConnecteur:

    • une instanciée dans l'AppDelegate
    • une instanciée lors de la désérialisation du xib.

    Le problème était que les outlets pointaient sur celle du xib, donc forcément, les outlets de MCNConnecteur n'étaient pas fixées (= nil = 0x00000000) et rien ne se passait.
    Appeler une méthode sur nil est légal en ObjC. Ça ne fait rien, mais c'est permis. (mais pas en Swift!)

    J'ai bien posé un break point dans -[applicationDidFinishLaunching:] mais texteChampTexte n'est pas vraiment bavard...

    C'est parce qu'il est défini en variable locale. Dès que le code sort de la portée, il est désalloué.

    Je crois comprendre l'idée de ce que tu diagnostiques là :) mais quant à le mettre en application... :/
    J'ai tenté un "@property MCNconnecteur *monMCNConnecteur;" mais c'est peut-être une énorme co...rie !?
    Accepté en compilation mais pas efficace...

    Si, si, c'est bien ainsi, qu'on déclare une propriété (~= une variable d'instance). Mais je t'ai expliqué plus haut pourquoi ça ne pouvait pas fonctionner (pas la bonne instance).

  • Merci Céroce pour toutes ces précisions que je m'efforce de comprendre au mieux :s
    Je crains cependant d'abuser avec de nouvelles demandes de précisions qui vont sans doute te sembler des évidences...

    @Céroce a dit :
    Ce qui faut comprendre est que les outlets et les actions se tirent normalement vers un contrôleur; typiquement un NSWindowController (ou un NSViewController si tu veux découper ta fenêtres en sous-vues, si elles doivent être réutilisées ou que la fenêtre est complexe).

    Je prends bonne note de tout ça :)

    Celui-ci peut être instancié dans le xib ou storyboard.

    Ne l'est-il pas forcément ? Car pour tirer des outlets vers le code du contrôleur, une 'instance' de ce contrôleur doit être présente graphiquement dans le XIB, non ?

    Pour le modèle, tu n'en n'as pas défini dans ton exemple. Ça peut être un simple héritier de NSObject.
    Ça amène une question: où instancier l'objet racine du Modèle ?

    • Si l'application utilise des documents, ce sera dans une sous-classe de NSDocumentController.
    • Si l'application n'utilise pas de documents (un seul stockage pour toutes les données), alors généralement, l'objet racine sera instancié par l'AppDelegate.

    J'en prends bonne note également :)

    Dans les deux cas, il faudra déclarer une propriété pour conserver l'objet.

    Je comprends, dans mon cas d'une appli sans document, que je dois instancier un "ModeleNSObject" dans l'AppDelegate et déclarer dans ce même AppDelegate qqchose comme "@property ModeleNSObject *monModeleNSObject;"
    C'est bien ça ?

    @Céroce a dit :
    Il faut comprendre un minimum comment ça marche: un objet va instancier un NSWindowController qui va charger un xib/storyboard. Ce fichier dit: je comporte tels objets avec telles propriétés. J'ai des outlets qui pointent vers File's Owner (ou vers un objet interne au xib/storyboard).

    Oops :#
    Là j'avoue ça va trop vite pour moi...
    Toujours dans mon cas de figure, quand tu dis 'un objet va instancier...' ça signifie que dans la classe du Modèle je dois instancier un Contrôleur qui va donner accès aux objets de la Vue puisqu'il a tous les outlets nécessaires ?
    Du genre, pour le code de AppDelegate.m :

    import "AppDelegate.h"
    @interface AppDelegate ()
    @property (strong) IBOutlet NSWindow *window;
    @property MCNmodele *monMCNmodele;
    @end
    @implementation AppDelegate
    @synthesize monMCNmodele;
    - (void)applicationDidFinishLaunching:(NSNotification *)aNotification {
    // Insert code here to initialize your application
    MCNmodele *monMCNmodele = [[MCNmodele alloc] init];
    [monMCNmodele modifParCode];
    }
    - (void)applicationWillTerminate:(NSNotification *)aNotification {
    // Insert code here to tear down your application
    }
    @end

    Mieux qu'un 'corrigé, tu aurais un exemple de code ?

    Qui est File's Owner ? L'objet qui instancie le xib/storyboard, donc le NSWindowController.
    Ainsi, disons que le NSTextField est instancié à partir du xib, et que l'outlet est appelée nameTextField, ça va copier la valeur du pointeur sur le NSTextField dans la propriété nameTextField du NSWindowControll@endrohid
    Ça utilise un mécanisme qu'on appelle le «Key-Value coding», qui permet de fixer une propriété dynamiquement en utilisant son nom. D'ailleurs tu vas bientôt faire une erreur à mélanger les outlets, et tu auras un message dans la console du type «Key error: Undefined key 'nameTextField'»

    Oops Oops :# :s
    Je vais dormir là dessus et m'y repencher un peu plus tard... :/

    Alors tu avais deux instances différentes de MCNConnecteur:

    • une instanciée dans l'AppDelegate
    • une instanciée lors de la désérialisation du xib.
      Le problème était que les outlets pointaient sur celle du xib, donc forcément, les outlets de MCNConnecteur n'étaient pas fixées (= nil = 0x00000000) et rien ne se passait.

    C'est donc une de trop si je comprends, et celle sur laquelle il faut travailler est uniquement celle du XIB...
    Mais comment invoquer dans le code précisément cette instance là ?

    Si, si, c'est bien ainsi, qu'on déclare une propriété (~= une variable d'instance). Mais je t'ai expliqué plus haut pourquoi ça ne pouvait pas fonctionner (pas la bonne instance).

    Et cette déclaration est à faire dans la classe en question, ou bien dans chacune de celles souhaitant accéder à la propriété

    Désolé d'avoir été long, et sans doute un peu lourd.
    Merci encore !

    Bien cordialement.

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