Ajouter des colonnes avec du code à un NSTableView controlé par un NSArrayController

yannSyannS Membre

Bonjour,

Après mettre fait les dents sur NSOutlineView avec un NSTreeController je commence à regarder NSTableView et NSArrayController.
Je n'ai pas de problème pour créer un tableau avec un nombre donné de colonnes et afficher des lignes de données.
Là je voudrais ajouter via du code des colonnes au NSTableView , de faire le binding de la même manière vers le NSArrayController
Est-ce possible ?
Si oui comment on modifie la classe qui gère la structure de données car elle doit elle aussi évoluer non ?

Je ne sais pas si je suis très clair
Un exemple peut-être,
J'ai une classe qui contient simplement nom, prénom et âge, cette classe est "rattachée" au NSArrayController qui est relier au NSTableView
Ce qui pourrait donner à l'affichage

NOM | PRENOM | AGE
-------|------------|------
toto | xxxxxx | 23
titi | xxxxxxx | 25
.....

Maintenant je voudrais ajouter par exemple la colonne "ADRESSE"

Réponses

  •     let nameColumn = NSTableColumn(identifier: NSUserInterfaceItemIdentifier(rawValue: "adresse"))
    
        nameColumn.width = 200
    
        nameColumn.headerCell.title = "Adresse"
    
        myTable.addTableColumn(nameColumn)
    
  • CéroceCéroce Membre, Modérateur
    30 janv. modifié #3

    Tu pourras ensuite binder par le code.
    Voir les méthodes du protocole NSKeyValueBindingCreation.

  • yannSyannS Membre

    merci pour vos réponses, j'ai créé la colonne , j'ai ajouté le bind

    nameColumn.bind(NSBindingName(rawValue: "value"), to: AC, withKeyPath: "arrangedObjects", options: nil)

    AC étant mon ArrayController,
    Mais je dois aussi faire le binding vers table view cell comme on le ferait dans IB ?

    Après il faudrait aussi que l'ArrayController prenne en charge "Adresse", j'avoue n'avoir pas vu comment faire, ça voudrait dire aussi modifier la classe à laquelle il est rattaché.
    Dans d'autre langage je vois comment faire mais pas avec swift

    Finalement ce ne serait pas plus simple d'utiliser les delegate du NSTableView ?

  • yannSyannS Membre

    Pardon pas les delegate de NSTableView mais les methodes de NSTableViewDataSource

  • CéroceCéroce Membre, Modérateur

    @yannS a dit :
    merci pour vos réponses, j'ai créé la colonne , j'ai ajouté le bind

    Mais je dois aussi faire le binding vers table view cell comme on le ferait dans IB ?

    Oui, c'est exactement pareil, sauf que je trouve ça plus clair avec le code qu'avec IB: au moins, on sait quels bindings ont du sens, alors qu'IB ne présente pas toujours les bons keypaths et a tendance à signaler des erreurs qui n'en sont pas (le fameux point d'exclamation).

    Après il faudrait aussi que l'ArrayController prenne en charge "Adresse", j'avoue n'avoir pas vu comment faire, ça voudrait dire aussi modifier la classe à laquelle il est rattaché.

    NSArrayController gère une liste d'objets de la même classe.
    Dans ton exemple, tu dis que ta classe ne possède pas de champ .adresse, il faudrait donc l'ajouter.

    Finalement ce ne serait pas plus simple d'utiliser les datasources du NSTableView ?

    Je ne vais pas relancer le débat sur les bindings que je trouve compliqués.
    Ceci dit, note que les bindings fonctionnent dans les deux sens: à la fois pour présenter les données mais aussi les modifier. Or il est complexe de modifier les données dans une table view en s'appuyant sur NSTableViewDataSource/Delegate. Quand on dit qu'AppKit est vieillissante, c'est pour ce genre de choses.

  • PyrohPyroh Membre

    En tant que défenseur du binding je devrai déjà m'être mêlé de ce thread mais j'avoue que je ne vois pas comment faire. Ajoutons à ça que j'ai du boulot par dessus la tête et que je suis en plein dans les procédures d'achat immobilier.

    MAIS. Je vais essayer d'y regarder. Avec de la chance ce soir je rentre tôt et Madame tard ce qui me laissera du temps pour geeker....

    Il y a juste un truc qui me chiffonne au passage c'est que tu bind directement arrangedObjects sur la colonne alors qu'il ne faut pas. Enfin plus avec les view-based tableviews. Il faut plus faire le binding sur le NSTextField de la cellule avec le path objectValue.property.

  • yannSyannS Membre

    Il y a juste un truc qui me chiffonne au passage c'est que tu bind directement arrangedObjects sur la colonne alors qu'il ne faut pas. Enfin plus avec les view-based tableviews. Il faut plus faire le binding sur le NSTextField de la cellule avec le path objectValue.property.

    Oui j'ai fait n'importe quoi, j'avoue que je me perds encore... le manque d'expérience
    Je recommencé de 0 en me basant également sur : http://www.knowstack.com/swift-create-nstableview-programatically/

    J'ai fait ceci

    class ViewController: NSViewController {
    
        @IBOutlet var arrayController: NSArrayController!
        @IBOutlet weak var tableView: NSTableView!
    
        let tailleColonneParDefaut:Float = 100.0
    
        let listeColonnes = [
            ["id":"adresse","titre":"Adresse","typeColonne":"text","tailleMax":500,"tailleMin":50],
            ["id":"codepostal","titre":"codepostal","typeColonne":"text","tailleMax":500,"tailleMin":50]
        ]
    
    
        override func viewDidLoad() {
            super.viewDidLoad()
    
            //Création d'un objet de base et implémentation dans l'arrayController
            let individu = personne("Mon Nom", "Mon Prenom")
            arrayController.addObject(individu)
    
    
            for nouvelleColonne in listeColonnes {
                let colonne = NSTableColumn(identifier: NSUserInterfaceItemIdentifier(rawValue: (nouvelleColonne["id"] as? String)!))
                colonne.headerCell.title = nouvelleColonne["titre"]! as! String
                colonne.width = CGFloat(tailleColonneParDefaut)
                colonne.minWidth = CGFloat(Float((nouvelleColonne["tailleMin"] as? Int)!))
                colonne.maxWidth = CGFloat(Float((nouvelleColonne["tailleMax"] as? Int)!))
                if (nouvelleColonne["typeColonne"] as! String == "check"){
                    let checkBox = NSButtonCell()
                    checkBox.setButtonType(.switch)
                    checkBox.title = ""
                    checkBox.alignment = .right
                    colonne.dataCell = checkBox
                }
    
                let sortDescriptor = NSSortDescriptor( key: nouvelleColonne["id"]! as? String, ascending: true, selector: #selector(NSNumber.compare(_:)))
                colonne.sortDescriptorPrototype = sortDescriptor
                tableView.addTableColumn(colonne)
    
                let identifiant = NSUserInterfaceItemIdentifier(rawValue: (nouvelleColonne["id"] as? String)!)
    
                let vc = tableView.makeView(withIdentifier: identifiant, owner: self) as? NSTableCellView
                vc?.bind(NSBindingName(rawValue: "value"), to: "", withKeyPath: "objectValue.nom", options: nil)
    
            }
    
    
        }
    
        override var representedObject: Any? {
            didSet {
            // Update the view, if already loaded.
            }
        }
    
    
    }
    

    J'avoue que je ne suis pas certain d'avoir fait correctement le bind

    Dans ton exemple, tu dis que ta classe ne possède pas de champ .adresse, il faudrait donc l'ajouter.

    Je ne vois pas comment l'ajouter pendant l'exécution, en php ou js par exemple je vois, mais là pas vraiment

  • CéroceCéroce Membre, Modérateur

    @yannS a dit :

    Dans ton exemple, tu dis que ta classe ne possède pas de champ .adresse, il faudrait donc l'ajouter.

    Je ne vois pas comment l'ajouter pendant l'exécution, en php ou js par exemple je vois, mais là pas vraiment

    En fait, je ne comprends pas quel est le problème. Ne peux-tu pas écrire:

    struct Pupil {
        var firstName: String
        var lastName: String
        var age: UInt
        var address: String
    }
    
  • PyrohPyroh Membre

    Je pense que pour pouvoir t'aider convenablement il faudrait que tu nous explique ce que tu cherche à faire. On a bien compris que tu voulais ajouter une colonne dans une table. C'est contexte qu'il serait intéressant de nous expliquer. Parce que si ça se trouve tu essaie de faire une chose impossible à réaliser au vu des technos que tu emploie.
    Si l'idée est d'avoir un tableau complètement dynamique avec un modèle qui l'est également il vaut mieux utiliser un NSDictionaryController au lieu d'un NSArrayController. Mais j'attendrai ta réponse pour en expliquer d'avantage.

  • yannSyannS Membre

    En fait j’ai développé une appli web avec extjs. C’est une sorte de Msquery (en plus poussé) ou d’impromptu pour ceux qui connaissent.
    Pour faire simple, dans l’appli on a une arbo (le catalogue) qui contient les données des tables de la base de données sur laquelle on souhaite faire une requête SQL.
    Chaque donnée peut être « drag and droppée » soit vers une autre arbo qui permet de constituer la filtre de la requête (le where...) ou vers une grille totalement vide (le select...)
    En « droppant » l’item de l’arbo catalogue dans la grille on génère une nouvelle colonne avec différents éléments (format, table origine, donnée, alias, tri,....)
    Quand on a fini de créer les parties where et select. les 2 éléments sont récupérés, encodés en JSON et envoyés à un serveur qui génère du code SQL et exécute la requête.
    Le requête est traitée et retourne un résultat.
    Finalement les données sont mise en place dans les colonnes correspondante de la grille.

    A tout moment on peut enlever, déplacer, ajouter des données (colonnes dans la grille) et relancer la requête.

    Je voudrais refaire mon appli sur mac.
    Coté arbo pas de problème, j’ai testé tout les éléments dont j’aurais besoin.
    Côté grille c’est moins évident

    Dans extjs normalement ce comportement n’est pas prévu tout du moins aussi directement, j’ai surchargé certaines classes utilisées par l’objet grille pour y arriver.
    Dans ext la grille a l’équivalent du NSTableColumn qui fonctionne presque pareil.
    Pour gérer les données la grille utilise une classe model qui est équivalente à NSArrayController ou certainement à NSDictionnaryController à la différence qu’il n’y a pas besoin d’y attacher une classe
    Dans model j’ai juste besoin de définir les fields ( au départ vide).
    En ajoutant une colonne j’ajoute dans le tableau des fields la description de la donnée.
    Puis dans la grille j’ajoute la colonne en indiquant comme donnée de rattachement l’id du field créé précement
    Je voudrais retrouver un comportement équivalemment

    Voilà, j’ai été long mais j’espère plus clair

  • PyrohPyroh Membre

    Contrairement au JavaScript qui est un language prototype Swift ne permet pas d'ajouter des propriétés dans une classe ou structure à la volée. Tu peux retrouver un comportement plus ou moins similaire avec un Dictionary au prix de performances diminuées mais sur ce coup tu n'as pas le choix.

    Ce qu'il te faut pour commencer c'est un NSArrayController parce que tu vas avoir n entrées. Ensuite il te faut un Dictionay par entrée qui remplace ta classe. Comble de bonheur func value(forKey key: String) -> Any? va aller directement piocher dans les valeurs que l'instance contient. C'est cool parce que le binding fonctionne avec ça (renseigne toi sur ce que sont les paradigmes KVO et KVC).

    Si on résume tu as un [[String:AnyObject?]] qui est géré par une instance de NSArrayController. Tu bind le content de ta table sur la propriété arrangedObjects du contrôleur sus-nommé et après je pense que tu peux te débrouiller pour ajouter dynamiquement tes colonnes.

    Par contre si je peux te donner un dernier conseil ce sera celui ci : tu fais du Swift sur ce projet alors ne le code pas comme du JavaScript. Si on regarde ton code et qu'on prend listeColonnes c'est typiquement le code de quelqu'un qui est perdu parce qu'il ne peut plus faire de JSON et qu'il imite son comportement avec des hash-maps. Il ne faut pas.
    Là il convient de définir une nouvelle struct qui fera le boulot parce c'est plus propre, ça évite d'écrire ce genre d'horreur : nouvelleColonne["id"] as? String et en terme de performances c'est incomparable.

  • Bonjour,

    Je suis nouveau sur ce forum (ainsi qu’en programmation) et Google m’a pointé sur ce fil qui concerne exactement mon problème. Avez-vous trouvé une solution ?

    Je souhaite utiliser une NSTableView (ViewBased) qui utilise le moins possible IB.

    Dans IB je ne crée que la table et une colonne avec un id "ModelCellView". let myCellViewNib = tableView.registeredNibsByIdentifier![NSUserInterfaceItemIdentifier(rawValue: "ModelCellView")]

    C’est cette colonne que je copie ensuite avec des ID différents.
    let newColumn = NSTableColumn(identifier: NSUserInterfaceItemIdentifier(rawValue: columnDictionary["columnIdentifier"] as! String))

    Tout fonctionne parfaitement bien, mais lorsque j’édite la table les données ne se mettent pas à jour. C’est normal mais pas le comportement que je souhaite implémenter.

    J’ai essayé différentes méthodes, principalement binding manuel et « action » sans succès.

    Un truc m’échappe très clairement et comme il semble qu’il y ait des experts sur ce site j’aimerai bien profiter de vos conseils si possible.
    J’ai monté mon code sur https://github.com/Lavallette/NSTableView-Sample.git. Il est très simple. :o .

    Dans mon cas quel est la meilleure façon de lier la table au model?

    Merci d’avance.

  • Je ne sais pas si cette réponse m'était destinée mais cette solution ne s'applique pas à mon cas de figure car il est précisé "_This method is intended for use with cell-based table views, _".

    A moins que mon manque d'expérience me joue des tours, ce qui est tout à fait possible.

  • Contrairement au JavaScript qui est un language prototype Swift ne permet pas d'ajouter des propriétés dans une classe ou structure à la volée. Tu peux retrouver un comportement plus ou moins similaire avec un Dictionary au prix de performances diminuées mais sur ce coup tu n'as pas le choix.
    .....
    Là il convient de définir une nouvelle struct qui fera le boulot parce c'est plus propre, ça évite d'écrire ce genre d'horreur : nouvelleColonne["id"] as? String et en terme de performances c'est incomparable.

    Merci pour les conseils, bon après 8 ans à ne bosser exclusivement que sur des RIA avec des backend en php on à des habitudes :)
    J'avoue avoir encore des (gros) progrès à faire pour maitriser le mode de fonctionnement préconiser.
    Je vais bosser dessus dans le temps.
    En attendant je continue à expérimenter les NSTableView mais sans binding (pour l'instant).
    J'ai repris mon test que j'ai modifié pour utiliser

    tableView(_ tableView: NSTableView, viewFor tableColumn: NSTableColumn?, row: Int) -> NSView?

    Voici ce que ça donne:

    class ViewController: NSViewController, NSTableViewDelegate, NSTableViewDataSource {
    
        @IBOutlet weak var tableView: NSTableView!
    
        let tailleParDefaut:Float = 100.0
        let tableauColonnes = [
            ["id":"prenom","titre":"Nom","typeColonne":"text","tailleMax":500,"tailleMin":50],
            ["id":"nom","titre":"Prenom","typeColonne":"text","tailleMax":500,"tailleMin":50],
            ["id":"age","titre":"Age","typeColonne":"num","tailleMax":500,"tailleMin":50],
            ["id":"salaire","titre":"Salaire","typeColonne":"text","tailleMax":500,"tailleMin":50],
            ["id":"pleinTemps","titre":"Plein temps","typeColonne":"check","tailleMax":500,"tailleMin":50]
        ]
    
        let donnees : NSMutableArray = [
            ["prenom":"Debasis","nom":"Das","age":"26","salaire":"10000","pleinTemps":1],
            ["prenom":"John","nom":"Doe","age":"26","salaire":"10000","pleinTemps":1],
            ["prenom":"Jane","nom":"Doe","age":"26","salaire":"10000","pleinTemps":0]
        ]
    
        override func viewDidLoad() {
            super.viewDidLoad()
    
            // Do any additional setup after loading the view
    
            supprimerToutesLesColonnes()
            chargerLesColonnes()
    
        }
    
        override var representedObject: Any? {
            didSet {
            // Update the view, if already loaded.
    
            }
        }
    
    
    
    
        func supprimerToutesLesColonnes(){
            let tColCount = self.tableView!.tableColumns.count
            if tColCount > 0{
                for _ in 0..<tColCount{
                    tableView!.removeTableColumn((tableView?.tableColumns[0])!)
                }
            }
        }
    
    
        func chargerLesColonnes(){
    
            for colonne in tableauColonnes {
    
                let nouvelleColonne = NSTableColumn(identifier: NSUserInterfaceItemIdentifier(rawValue: (colonne["id"] as! String)))
                nouvelleColonne.headerCell.title = (colonne["titre"] as! String)
                nouvelleColonne.headerCell.alignment = .center
                nouvelleColonne.width = CGFloat(tailleParDefaut)
                nouvelleColonne.minWidth = CGFloat(Float((colonne["tailleMin"] as! Int)))
                nouvelleColonne.maxWidth = CGFloat(Float((colonne["tailleMax"] as! Int)))
                let sortDescriptor = NSSortDescriptor( key: (colonne["id"] as! String), ascending: true, selector: #selector(NSNumber.compare(_:)))
                nouvelleColonne.sortDescriptorPrototype = sortDescriptor
                nouvelleColonne.bind(NSBindingName.value, to: self, withKeyPath: "self", options: nil)
    
                tableView.addTableColumn(nouvelleColonne)
    
            }
        }
    
        // MARK: - Delegate du tableView
        func numberOfRows(in tableView: NSTableView) -> Int {
            let numberOfRows:Int = self.donnees.count
            return numberOfRows
        }
    
    
        func tableView(_ tableView: NSTableView, viewFor tableColumn: NSTableColumn?, row: Int) -> NSView? {
    
            let ligne = self.donnees[row] as! [String:Any]
            let donnee = ligne[(tableColumn?.identifier.rawValue)!]
    
            var cellule = tableView.makeView(withIdentifier: (tableColumn?.identifier)!, owner: self) as? NSTableCellView
            if(cellule == nil){
                cellule = NSTableCellView()
                cellule!.identifier = tableColumn?.identifier
            }
            cellule!.textField?.stringValue = donnee as! String
    
            return cellule;
    
        }
    
    
        func tableView(_ tableView: NSTableView, sortDescriptorsDidChange oldDescriptors: [NSSortDescriptor])
        {
            self.donnees.sort(using: tableView.sortDescriptors)
            self.tableView?.reloadData()
        }
    }
    

    Je rencontre un problème avec NSTableCellView, en fait ça n'affiche rien dans le NSTableView.
    Mais si je remplace le NSTableCellView par un NSTextView j'ai bien les données d'affichées.

    func tableView(_ tableView: NSTableView, viewFor tableColumn: NSTableColumn?, row: Int) -> NSView? {
                var textView = tableView.makeView(withIdentifier: (tableColumn?.identifier)!, owner: self) as? NSTextView
                if textView == nil {
                    textView = NSTextView()
                    textView?.identifier = tableColumn?.identifier
                }
                textView?.string = String(describing: donnee)
                return textView;
    
            }
    

    J'ai même essayé avec une checkBox et là aussi elle est bien affichée

    C'est liée au fait que le NSTableView est en mode "View based" ?

  • CéroceCéroce Membre, Modérateur

    @yannS a dit :
    C'est liée au fait que le NSTableView est en mode "View based" ?

    Ah oui, certainement!
    Pour t'expliquer, les NSCells sont en quelque sorte des vues poids-plume. C'est le fonctionnement historique, bien adapté aux performances des machines des années 90.
    Depuis macOS 10.7, on peut utiliser à la place des cells des NSViews; le fonctionnement est calqué sur le modèle d'iOS, et est bien plus pratique. Il n'y a pas de problèmes de performances grâce à l'accélération graphique matérielle et un système de recyclage des vues.

    Aujourd'hui, il y a peu de raisons d'utiliser une NSTableView cell-based. En fait, ça mériterait largement d'être retiré d'AppKit, ce qui simplifierait les API et Interface Builder.

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