Affichage NSButton

16:22 modifié dans API AppKit #1
J'ai deux problèmes à  soumettre à  un développeur sérieux qui maitrise bien Cocoa.

Je développe avec Xcode et Interface Builder à  l'aide de Cocoa en Objective-C sous 10.3.9.
La situation est simple. Je dispose d'une palette de contrôles (identique à  celle d' Interface Builder). Ces contrôles sont matérialisés par des objets NSImageView désactivés afin de récupérer la localisation lors d'un clic.
Ces objets sont déplaçables vers une fenêtre qui est l'éditeur de l'interface, comme pour Interface Builder.
La plupart des objets ne pose aucun problème à  l'exception de ceux de la classe NSButton et plus particulièrement ceux dont le paramétrage est le suivant

• ButtonType = NSMomentaryPushInButton & BezelStyle = NSRoundedBezelStyle
Il s'agit du bouton classique qui est bleuté lorsqu'il est par défaut.

• ButtonType = NSMomentaryPushInButton & BezelStyle = NSRegularSquareBezelStyle
Il s'agit du bouton permettant l'insertion d'un texte et/ou d'un image.

Le problème réside dans l'affichage. En effet, les bordures sont décalées par rapport au reste du bouton.
D'autre part, lorsqu'un objet de ce type est glissédéposé dans la fenêtre (plutôt la contentView, qui une instance d'une classe dérivée de NSView !). Son affichage est incorrecte. Par contre lorsqu'il est  déplacé dans l'éditeur, il reprends sa forme correcte. Ce comportement est valable pour les deux types de bouton.

Le second problème concerne l'affichage des poignées pour redimensionner les objets.
Les poignées ne sont pas correctement dessinées. Les parties masquées sont celles qui débordent sur le contrôles.

J'ai adapté le code de l'application Sketch fournie avec les outils de développement.

N'hésitez pas à  me contacter pour tout détail supplémentaire.
Un grand merci à  celle ou celui qui m'aidera.

Réponses

  • fouffouf Membre
    16:22 modifié #2
    Pourrais-tu nous doner un peu plus de détail . En particulier comment fais-tu exactement pour afficher le boutons. S'agit-il d'une image ou bien d'une sous-vue affichée dans la NSImagView ?


    En tous cas, bienvenue sur ce forum. Allez, hop, tournée générale  :p :p ...
  • 16:22 modifié #3
    D'abord je te remercie de ton acceuil.

    L'opération permettant l'installation d'un objet visuel dans la fenêtre consiste à  glisser/déposer une image NSImageView. Le presse-papier contient une chaà®ne détenant le type précis de l'objet à  construire dans la fenêtre.
    Ainsi lorsque le grag&drop s'est déroulé correctement, la dernière méthode invoquée est la suivante:

    //**********************************************************************************************************
    // concludeDragOperation
    //**********************************************************************************************************
    -(void)concludeDragOperation:(id <NSDraggingInfo>)sender
    {
    [self clearSelection]; // Supprime l'ancienne selection (poignees des autres controles)
    [self createGraphic:_newControlType with:sender]; // Construit le controle
    [_newControlType release]; // Supprime la copie de la donnes déterminée par le presse papier
    } /* concludeDragOperation */

    Voici le code qui installe le bouton dans la vue éditrice

    //**********************************************************************************************************
    // createGraphic
    //**********************************************************************************************************
    -(void)createGraphic:(NSString*)inImageName with:(id <NSDraggingInfo>)inSender
    {
    NSRect rect;

    // 1- Deselectionne tout
    [self invalidateGraphics:_graphics];
    // 2- Calcul de la taille du controle dans la vue de depot
    rect.origin = [self convertPoint:[inSender draggedImageLocation] toView:nil];
    rect.size = [[inSender draggedImage] size];
    rect.origin.y -= rect.size.height;
    rect = NSInsetRect(rect,-1,-1);
    // 3- Creation du controle
    if ([inImageName compare:@Button] == NSOrderedSame)
    _creatingGraphic = [self createCXButton:rect withButtonType:NSMomentaryPushInButton withBezelStyle:NSRoundedBezelStyle];

    etc...

    La classe CXButton est dérivée de NSButton. Elle apporte des modifications au comportement des boutons.
    Voici le code complet de la méthode init en creation.

    //**********************************************************************************************************
    // initWithFrame
    //**********************************************************************************************************
    -(id)initWithFrame:(NSRect)inFrame withButtonType:(NSButtonType)inType withBezelStyle:(NSBezelStyle)inStyle
    {
    if ((inType == NSRadioButton) || (inType == NSSwitchButton))
    inFrame.size.width += 50; // pour le texte
    self = [super initWithFrame:inFrame];
    if (self)
    {
    _name = nil;
    _controller = nil;
    _manipulatingFrame = NO;
    _Frame = inFrame;
    _origFrame = NSZeroRect;
    _superviewSaved = nil;
    _buttonType = inType;
    [self setButtonType:inType];
    if (inStyle != NoBezelStyle)
    [self setBezelStyle:inStyle];
    }
    return self;
    } /* initWithFrame */

    D'autre part, la super vue du bouton implémente la méthode suivante:

    //**********************************************************************************************************
    // drawRect
    //**********************************************************************************************************
    -(void)drawRect:(NSRect)rect
    {
        unsigned i;
        NSView* curGraphic;
        BOOL isSelected;
        NSRect drawingFrame;
        NSGraphicsContext*  currentContext = [NSGraphicsContext currentContext];

    // 1- Gestion de l'apparence pour le drag/drop
    if (_dragOperation)
    {
    if (_highlighted)
    {
    [[NSColor lightGrayColor] set];
    NSFrameRectWithWidth(rect,3.0);
    }
    }

    // 2- Gestion de l'affichage de la grille magnétique
        if ([gApp showGrid])
            DrawGridWithSettingsInRect([gApp spaceGrid], [NSColor lightGrayColor], rect, NSZeroPoint);

        i = [_graphics count];
        while (i-- > 0)
    {
            curGraphic = [_graphics objectAtIndex:i];
    // frame de l'objet visuel + taille des poignées (les poignées sont placées à  cheval sur l'objet)
            drawingFrame = [curGraphic drawingFrame];
            if (NSIntersectsRect(rect, drawingFrame))
    {
                if (! _gvFlags.knobsHidden)
    {
                    // Figure out if we should draw selected.
                    isSelected = [self graphicIsSelected:curGraphic];
                    // Account for any current rubberband selection state
                    if (_rubberbandGraphics && (isSelected == _gvFlags.rubberbandIsDeselecting) && [_rubberbandGraphics containsObject:curGraphic])
                        isSelected = (isSelected ? NO : YES);
                }
    else
                    // Do not draw handles on graphics that are editing.
                    isSelected = NO;
    // **** Je ne comprend rien en ce code, les contextes graphiques ?
                [currentContext saveGraphicsState];
    [curGraphic setNeedsDisplayInRect:drawingFrame]; // L'affichage du bouton s'effectue en invoquant drawRect du controle
    [currentContext restoreGraphicsState];
    if (isSelected)
    [curGraphic drawHandlesInView:self];
            }
        }
    // Dessine la zone de selection si elle existe
        if (! NSEqualRects(_rubberbandRect, NSZeroRect))
    {
            [[NSColor knobColor] set];
            NSFrameRect(_rubberbandRect);
        }
    } /* drawRect */

    La plupart du code provient de l'application sketch.
    Je pense que tu dispose de toutes les pièces. Si tu veux éclaircir un point particulier, pas de problème.
    Par avance, merci beaucoup.


  • fouffouf Membre
    16:22 modifié #4
    Salut.
    A vrai dire j'ai le souvenir d'avoir déjà  eut ce problème la une fois (le decalage d'une partie du bouton) mais je ne me souviens pas quand.
    Par contre, j'ai essayé de créer un bouton "standard" par le code :
    - (void)awakeFromNib<br />{<br />	NSButton *button;<br />	<br />	button = [[NSButton alloc] initWithFrame:NSMakeRect(30,30,100,30)];<br />	[button setButtonType:NSMomentaryPushInButton];<br />	[button setBezelStyle:NSRoundedBezelStyle];<br />	[[_window contentView] addSubview:button];<br />}&nbsp; <br />
    

    et ca marche très bien. Ce que je te propose donc tout d'abord, c'est de "laisser tomber" ta sous-classe CXButton et créer tes boutons "directement" dans la méthode createGraphics (au fait, pour tester la chaine inImageName, je te conseille d'utiliser isEqualTo: ou isEqualToString: plutot que compare: ) et tu regardes si ca marche.

    Petite Explication des contextes graphiques :
    Tu dois voir ca un peu comme des boites dans lesquelles tu vas imposer certains comportements (la couleur, le mode de fusion, l'ombre, ...). Ce sont des boites que tu "encastres" les unes dans les autres. Lorsque tu fais saveGraphicsContext, tu vas créer cette boite ; puis tu vas faire des appels du genre [[NSColor blueColor] set]; - tout ce qui va modifier les propriétées du dessin que tu vas effectuer - puis appeler tes procédures de dessin ([NSBezierPath fillRect:rect] par exemple) et enfin, lorsque tu auras terminé avec ces dessins nécessitant toutes ces propriétés, tu vas appeler restoreGraphicsContext pour revenir au status quo. Pour revenir aux boites, une boite donnée ne sait pas ce qu'il se passe dans les boites qu'elle contient. Ainsi saveGraphicsContext ajoute une boite sur la pile alors que restoreGraphicsContext l'enlève. Ce qu'il faut savoir c'est que les opérations de dessin seront toujours faites avec les propriétés de la boite qui est au dessus de la pile.

    Enfin, un dernier petit truc :  setNeedsDisplayInRect: comme setNeedsDisplay: impliquent certes des appels à  drawRect: mais en différé (tu ne sais pas quand ils auront lieu, mais ils auront lieu avant la fin de la run loop). Ainsi ici, les encadrer de save/restoreGraphicsContext ne sert à  rien - il vaut mieux faire ces appels dans le drawRect:
  • 16:22 modifié #5
    Bonjour,

    En ce qui concerne ton code, je l'avais déjà  essayé. Le résultat montre un bouton correctement dessiné.

    Une remarque sur la bizarrerie du défaut:

    Lorsque le bouton est déplacé sans l'activation du magnétisme, en fait le déplacement s'effectue au premier pixel, l'apparence reste inchangée. Dans le cas contraire, le bouton redevient normal ! C'est incompréhensible.

    J'ai même essayé de simuler un clic (mouseDown par NSEvent) sur le bouton nouvellement créé, aucun résultat.

    Par contre en ce qui concerne la sous classe. Elle est nécessaire pour les raisons suivantes:

    1°) Elle modifie le comportement du bouton lorsqu'il est considéré dans le mode conceptuel. C'est à  dire lorsque le clic se traduit par l'affichage des poignées sans modification de l'apparrence du bouton en question.

    2°)  Le mode conceptuel implique l'ajout de données membres pour le gérer. Par contre pour supporter ce dispositif intégralement , j'ai déclaré une catégorie sur NSView afin que chaque contrôle déposé dans la vue réponde correctement.

    3°) Le fonctionnement classique proposé par Cocoa ne permet que l'usage du couple (target,action). Il n'est pas possible d'implémenter les autres méthodes événementielles telles que mouseDown, etc., héritées de NSResponder. Le projet doit fournir tous les événements disponibles même si la plupart d'entre eux seront ignorés.

    Pour le reste, merci de ton explication, je vais cogiter pour l'essayer.

    A+
  • fouffouf Membre
    avril 2008 modifié #6
    Bon, je te dis ca comme ca, mais je ne suis pas sur que ca marche (ca expliquerait en tous cas certains trucs) essaye de faire un setFrame: avec des coordonnees entières (genre 134.0).

    [edit] Oui ca doit être ca puisque, lorsque je modifie un peu mon code en faisant initWithFrame:NSMakeRect(30,30.5,100,30) ca me donne la même chose que toi
  • 16:22 modifié #7
    javascript:void(0);
    Chapeau, tu as trouvé! Les deux controles qui posaient problèmes sont désormais affichés correctement.
    Très bien, et encore félicitation pour ton aide. Je n'aurais jamais trouvé car j'ai toujours raisonné au niveau du pixel, qui est par essence non fractionnable ! Je suis assez nouveau en Cocoa. J'ai longtemps utilisé PowerPlant de Metrowerks !
    Il faut en effet, extraire les parties entières des coorddonnées de la structure NSRect comme le montre le code:

    //**********************************************************************************************************
    // createCXButton
    //**********************************************************************************************************
    -(NSView*)createCXButton:(NSRect)inFrame withButtonType:(NSButtonType)inType
    withBezelStyle:(NSBezelStyle)inStyle
    {
    CXButton* button;
    NSString* name;
    NSRect frame;

    frame = NSInsetRect(inFrame,-2,0);
    frame.origin.x = (long)frame.origin.x;
    frame.origin.y = (long)frame.origin.y;
    button = [[CXButton alloc] initWithFrame:frame withButtonType:inType withBezelStyle:inStyle];
    name = [self newName:@Button];
    [self installName:name];
    [button setName:name];
    [button setTitle:name];
    return button;
    } /* createCXButton */
  • fouffouf Membre
    16:22 modifié #8
    Alors, un dernier petit truc, je pense que c'est un peu lourd de faire deux cast successifs pour récupérer la partie entière d'un float, la fonction floorf est, à  mon goût, meilleure et surtout plus lisible.

    A propos de pixels à  valeur non entière, on peut mentionner un comportement intéressant lorsque l'on fait des dessins : mettons que l'on veuille dessiner une droite horizontale ou vertical avec des NSBezierPath et d'épaisseur 1 px, et que l'on veut qu'elle fasse réellement 1px à  l'écran, il faut alors donner des coordonnés du style (x1, 2.5)-(x2, 10.5) pour une droite horizontale. En effet, sinon l'anti-aliasing fera son oeuvre et ca donne des trucs très moches ...

    Au plaisir de te revoir bientôt ...
  • AliGatorAliGator Membre, Modérateur
    16:22 modifié #9
    dans 1208979851:

    Alors, un dernier petit truc, je pense que c'est un peu lourd de faire deux cast successifs pour récupérer la partie entière d'un float, la fonction floorf est, à  mon goût, meilleure et surtout plus lisible.

    A propos de pixels à  valeur non entière, on peut mentionner un comportement intéressant lorsque l'on fait des dessins : mettons que l'on veuille dessiner une droite horizontale ou vertical avec des NSBezierPath et d'épaisseur 1 px, et que l'on veut qu'elle fasse réellement 1px à  l'écran, il faut alors donner des coordonnés du style (x1, 2.5)-(x2, 10.5) pour une droite horizontale. En effet, sinon l'anti-aliasing fera son oeuvre et ca donne des trucs très moches ...

    Au plaisir de te revoir bientôt ...
    C'est marrant c'est précisément le gros problème qu'on a avec le moteur de rendu qu'on développe pour notre application (sous Windows  :o ) au boulot... Dès qu'on tombe sur du subpixel, il faut choisir, soit l'arrondi au risque de décaler parfois d'un pixel le rendu attendu (rectangle de 45 pixels voulu, 44 pixels obtenus, car centré en 0 et 22.5 à  gauche, 22.5 à  droite, ça tombe sur du subpixel)... ou sinon l'antialias, qui fait des trucs sympas à  condition d'être bien réglé... mais qui n'est pas toujours adapté (pour du texte de petite taille ça a tendance à  dégrader la qualité et lisibilité en le rendant tout flou, pour des textures assez grandes ou quoi par contre c'est mieux avec)...

    Comme quoi on retrouve toujours les mêmes problèmes :D
  • schlumschlum Membre
    16:22 modifié #10
    Je ne sais pas si on peut appeler ça de l'anti-aliasing, mais en effet, si on dessine une droite d'un pixel de large entre deux lignes de pixels, il va dessiner sur ces deux lignes avec un coefficient alpha /2.


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