Zoom 4, gestión del teclado

23 agosto 2008

Esta última entrada sobre el zoom no trata sobre gráficos, sino sobre la gestión del teclado. Ya en otra ocasión traté sobre la Gestión de eventos de teclado. En aquella ocasión explique el sistema general de gestión de eventos en Cocoa y como recibir los carácteres para tratarlos en nuestro programa.

En esta ocasión quiero explicar cómo gestionar dos tipos de pulsaciones de teclado: las teclas de cursor y los atajos de teclado. En cualquier caso la mejor forma de tratar con el teclado es sobreescribir las funciones predefinidas en Cocoa.

Aceptar eventos de teclado

Lo primero siempre es informar al sistema que la vista en la que estamos es capaz de responder a los eventos. Para ello hay que sobreescribir la función acceptsFirstResponder.

- (BOOL)acceptsFirstResponder {
	// Permite que la vista tenga el foco.
	return YES;
}

Gestionar las pulsaciones de teclado

El siguiente paso es ser capaz de saber que hacer cuando se pulsa una tecla. Es decir hace falta sobreescribir la función keyDown:(NSvent).

- (void)keyDown:(NSEvent *)evento {
    // Las teclas de cursors estan asociadas al teclado numérico
    if ([evento modifierFlags] & NSNumericPadKeyMask) {
        [self interpretKeyEvents:[NSArray arrayWithObject:evento]];
    } else {
        [super keyDown:evento];
    }
}

Y es aquí dónde comienzan las cosas interesantes. Lo más importante és la llamada a la función [self interpretKeyEvents:(NSArray)] esto le indica al sistema que nuestra vista es capaz de gestionar los eventos de teclado. A cambio el sistema gestionará los atajos de teclado definidos en las preferencias del sistema y nos devolverá a nuestra vista los otros eventos que ocurran bien clasificados y definidos en la clase NSResponder.

Claro está que, en este momento, no nos interesa controlar todos los eventos de teclado, sino solamente aquellos relacionados con las teclas de cursor. Por ellos sólo se llama a la función interpretKeyEvents: si detectamos que los modificadores de estado (modifierFlags) nos indican a través de una máscara definida igualmente en NSResponder que estamos ante la pulsación de una tecla de cursor. Estos modificadores de estado nos marcan si hay alguna tecla especial pulsada (función, comando, alternativa, ..). Y curiosamente las teclas de cursor estan relacionadas con las teclas del teclado numérico (¿una herencia del IBM PC?). En caso que sea un evento que no nos interesa gestionar (cualquier otra pulsación de teclas) lo enviamos a la superclase para que se encarge.

Las teclas de cursor

El tercer paso es crear (sobreescribir) las funciones necesarias para gestionar las pulsaciones sobre las teclas de cursor. Cómo en este ejemplo quiero hacer que la vista vaya desplazando la ventana visible del scroll las funciones cambiarán el origen de scroll de la vista.

-(IBAction)moveUp:(id)sender
{
    NSPoint posicion=[self visibleRect].origin;
    posicion.y = posicion.y + 10;
    [self scrollPoint:posicion];
}

-(IBAction)moveDown:(id)sender
{
    NSPoint posicion=[self visibleRect].origin;
    posicion.y = posicion.y - 10;
    [self scrollPoint:posicion];
}

-(IBAction)moveLeft:(id)sender
{
    NSPoint posicion=[self visibleRect].origin;
    posicion.x = posicion.x - 10;
    [self scrollPoint:posicion];
}

-(IBAction)moveRight:(id)sender
{
    NSPoint posicion=[self visibleRect].origin;
    posicion.x = posicion.x + 10;
    [self scrollPoint:posicion];
}

Las funciones moveUp:, moveDown:, moveLeft: y moveRight: estan definadas (dónde sino) en NSResponder.

Atajos de teclado

Lo anterior no sirve si nos interesa definir atajos de teclado (key Bindings?). En esta ocasión queria reproducir el comportamiento de muchas aplicaciones de Apple, en las cuales apretando la combinación cmd+ se amplia la vista y con cmd- se reduce.

Pues resulta que los eventos que utilizan la teecla de comando no son eventos de teclado normales, sino que se tratan como pulsaciones equivalentes a una orden (en inglés performKeyEquivalent:). Entonces hay que buscarse la vida con esta nueva (para mi) función. Al igual que la orden interpretKeyEvents: hay que informar al sistema que la vista que estamos programando se puede encargar de ciertos eventos y dejar el resto para la superclase. Para ello la función ha de devolver un valor verdadero (YES) si es capaz de gestionar el evento.

El ejemplo es bastante más claro:

/**
* performKeyEquivalent:(NSEvent)
*
* Gestiona los atajos de teclado. Si no corresponde a uno de los gestionados
* lo reenvia a la superclase para la gestión.
*
*/
- (BOOL)performKeyEquivalent:(NSEvent *)evento {
	NSString *caracteres = [evento charactersIgnoringModifiers];
	unichar keyChar = ([caracteres characterAtIndex: 0]);
	BOOL retVal = NO;
	switch(keyChar) {
        float nuevaEscala = 0;
		case '+':
            nuevaEscala = factorEscala*2;
            [self setFactorEscala:nuevaEscala];
			retVal = YES;
			break;
        case '-':
            nuevaEscala = factorEscala/2;
            [self setFactorEscala:nuevaEscala];
			retVal = YES;
			break;
		default:
			retVal = [super performKeyEquivalent:evento];
	}
	return retVal;
}

La función recoge el evento y si la tecla pulsada és un “+” o un “-” amplia o reduce la escala. En caso contrario llama a la superclase para gestionar el evento. La sencillez de la función no tiene nada que ver con el tiempo que he tenido que dedicar a investigar estos atajos de teclado. Al final encontre la solución en uno de los ejemplos de la documentación de Apple AXCanvas (/Developer/Accessibility/AXCanvas).

Más información en

– Apple: Cocoa Event-Handling Guide.

Entradas Relacionadas

Factormac.com

22 agosto 2008

Más enlaces relacionados con la programación en Cocoa.

En esta ocasión sobre FactorMac.com un interesante blog sobre los productos de Apple en general. En este caso el apartado que a mi me interesa el pequeño ejemplo que desarrollan sobre como hacer un programa en XCode: Programación en Cocoa y Objective-C. Es uno de los pocos ejemplos en castellano que muestran como comenzar a trabajar con XCode.

Zoom 3, desplazando la ventana de scroll

19 agosto 2008

En entradas anteriores he escrito sobre cómo crear en Interface Builder una ventana con barras de desplazamiento y también sobre cómo ampliar la vista. Ahora ya toca un ejemplo concreto. Una pequeña aplicación que hace un dibujo y que tiene la opción de hacer zoom sobre este dibujo.

La ventana del ejemplo

La ventana del ejemplo

Deslizador

La primera novedad sobre los programas anteriores es la aparición de un deslizador (NSSlider), este deslizador será el que permitirá decidir sobre el nivel de zoom que tendremos en la vista.

En Interface Builder definimos las propiedades del deslizador, en esta ocasión marcar los valores mínimo (1.0), máximo (8.0) y valor con el que se comienza, current, (1.0). También es conveniente dejar sin marcar la casilla “Continuous” que activa el control contínuo del deslizador. El problema del control contínuo es la gran carga de trabajo que puede suponer para el sistema, ya que se realiza la acción para cada pequeño cambio de la posición aunque no se suelte el ratón.

Propiedades del deslizador en Interface Builder

Propiedades del deslizador en Interface Builder

Para poder hacer zoom con el deslizador hay que preparar en la vista una acción (IBAction).

-(IBAction) escogerEscala:(id)sender{
    [self setFactorEscala:[sender floatValue]];
}

Y para que si cambiamos el nivel de zoom de otras formas (con el teclado, por ejemplo) una salida (IBOutlet) para dar al deslizador el nuevo valor.

// Nos aseguramos que el deslizador tenga el valor adecuado
// del factor de escala
[deslizador setFloatValue:factorEscala];

Scroll

El nucleo del programa esta en dos elementos. En una variable que llamaré factorEscala, que guarda el nivel actual de zoom. Y en la función setFactorEscala: que establece el nuevo nivel de zoom. Para el nuevo nivel de zoom la función hará tres cosas:

  1. Solo activa la función si ha cambiado el valor de escala y luego asegurarse que el nuevo factor de escala está entre 1 y 8.
  2. Ampliar el marco de la vista para hacer el zoom.
  3. Centrar la vista en el centro de la ventana actual.

Es posible (incluso recomendable) el zoom hacerlo en una función diferente, pero para un ejemplo tan sencillo no he querido complicarme la vida. La parte de centrar la vista es prescindible, y sospecho que no dentro de mucho esta opción se controlará a través de Interface Builder.

/**
* setFactorEscala: (float)nuevaEscala
*
* Modifica el tamaño de la vista para hacer zoom.
* Se encarga de mantener la escala entre 1 y 8, de centrar la vista.
*/
-(void) setFactorEscala:(float)nuevaEscala{
 if(nuevaEscala != factorEscala) {
 // Limitamos los posibles valores de escala a [1, 8]
 if (nuevaEscala > 8)
 nuevaEscala = 8;
 if (nuevaEscala < 1)
 nuevaEscala = 1;

 factorEscala = nuevaEscala;

 // Para hacer zoom, necesitamos mantener los límites y cambiar
 // el tamaño del marco (frame)
 NSSize frameSize, boundsSize;
 frameSize = boundsSize = [self bounds].size;

 // Buscamos las coordenadas del centro actual de la vista
 NSPoint nuevoOrigen;
 NSRect clip = [self visibleRect];
 nuevoOrigen.x = clip.origin.x+clip.size.width/2;
 nuevoOrigen.y = clip.origin.y+clip.size.height/2;

 //
 frameSize.height *= factorEscala;
 frameSize.width *= factorEscala;
 [self setFrameSize:frameSize];
 [self setBoundsSize:boundsSize];

 // Calculamos el nuevo origen a
 // partir del centro anterior de la vista.
 clip = [self visibleRect];
 nuevoOrigen.x -= clip.size.width/2;
 nuevoOrigen.y -= clip.size.height/2;

 // Desplazamos la vista al nuevo origen
 [self scrollPoint:nuevoOrigen];

 // Nos aseguramos que el deslizador tenga el valor adecuado
 // del factor de escala
 [deslizador setFloatValue:factorEscala];

 // Al Cambiar límites y la posición de Scroll
 // no hace falte pedir un redibujado
 //[self setNeedsDisplay:YES];
 }
}

Puedes ver el código fuente del ejemplo en: zoom.zip

 

Archivos alojados en Dropbox.com

 

Faltará por explicar unos “detallitos”, cómo gestionar el nivel de zoom con el teclado. Ya que es algo bastante diferente al apartado gráfico lo explicaré en una entrada propia.

Entradas Relacionadas

El rincón del C

18 agosto 2008

Un clásico entre los mejores de la programación en castellano. El rincón del C.

Objective-C el lenguaje de programación que utilizamos para programar en Mac OSX es un derivado del lenguaje C. No solamente eso, sino que la influencia del C en cualquier lenguaje de programación es immensa. De forma que es interesante como mínimo conocer este lenguaje.

Además si eres un autentico novato en programación el curso de C, es de lo mejor que se puede encontrar para iniciarse.

Zoom 2, geometría de la vista

10 agosto 2008

Siguiendo con las entradas relacionadas com el zoom y el Scroll en un programa Cocoa, ahora toca explicar los parámetros que marcan la visibilidad de una vista. La geometría de la vista.

Marco y límites

En el momento de definir cómo se verá una vista por la pantalla hay dos parámetros básicos: el marco (frame) y los límites (bounds). Ambos son datos del tipo NSRect, es decir nos dan un punto de origen y unas medidas de ancho y alto.

  • El marco (frame) define la situación de la vista respecto a la “supervista”, es decir la vista o ventana que contiene a la vista que estamos tratando. Así pues las coordenadas del punto de origen estan referidas a la “supervista”.
  • Los límites (bounds) definen la parte de la vista que queremos mostrar, siempre refidas al sistema de coordenadas de la propia vista.

En la documentación que proporciona Apple encontramos unas imágenes muy esclarecedoras. La primera nos muestra la relación entre el marco (derecha) y los límites (izquierda). También puede verse que en una situación normal las medidas de marco y límites coinciden.

Relación entre marco y limites

La segunda imagen nos muestra que pasa si la medida del marco es más grande que los límites, la imagen se dimensiona hasta ocupar todo el marco:

Escalado de la imagen para ocupar todo el marco

Lo mismo pasaria si los límtes fueran más grandes que el marco, solo que en ese caso la imágen aparecería más pequeña. Este comportamiento nos permitirá hacer zoom sobre una imagen con facilidad.

La combinación de los dos parametros puede dar lugar a que no toda la vista sea visible, por ejemplo:

Relación entre marco y limites

Si realmente queremos conocer que parte de la vista es realmente visible necesitamos el método visibleRect que nos devuelve un NSRect con el origen y las medidas de la parte visible.

Pruebas

Que pasa ¿No ha quedado del todo claro? Pués a mí támpoco.

Así que decidí hacer mi propio programa, muy simple, para explorar las diferentes posibilidades. Es una variación del programa de la entrada anterior. En este caso además del gráfico he añadido unos botones que dan la posibilidad de cambiar el marco y los límites de la vista. Es posible ver que pasa cuando se envia solamente la orden de cambiar el marco, solamente cambiar los límites y que pasa cuando cambiamos marco y límites dejando el otro parámetro inalterado.

Además también tenia la duda sobre que pasaba con los eventos de ratón al cambiar la vista, de forma que he añadido una pequeña etiqueta que muestra la posición del último lugar donde se ha hecho clic con el ratón.

Para cambiar el tamaño del marco se usa el método setFrameSize: que espera un parámetro del tipo NSSize (ancho y alto), por ejemplo la orden:

[self setFrameSize:NSMakeSize(400, 400)];

Cambia el tamaño a 400 puntos x 400 puntos.

De forma similar los límites se cambian con el método setBoundsSize:.

Y el código para mostrar el clic del ratón será:

- (void)mouseDown:(NSEvent *)e{
    NSPoint punto = [self convertPoint:[e locationInWindow] fromView:nil];
    NSString *texto = [NSString stringWithFormat:@"(%1.0f, %1.0f)", punto.x, punto.y];
    [coordenadas setStringValue:texto];
}

Y aquí el código completo del programa: Zoom2.zip

Resultados

Marco y limites ampliados

Marco y límites ampliados

1. Ampliar marco y límites: tenemos una imágen del mismo tamaño que la imagen original y una vista ampliada en la que podemos desplazarnos con las barras de desplazamiento. Se pueden ver elementos de la imagén que anteriormente quedaban fuera de la vista.

2. Ampliar marco: el mismo resultado que antes. Resulta que al dar la orden setSizeFrame de forma automática también se amplian los límites de la imagen.

3. Ampliar marco manteniendo los límites: tenemos ampliación de la imagén. Como se ha mantenido el marco, ahora aparece las barras de desplazamiento parapoder observar la imagén. Los elementos que antes no se veían siguen sin verse.

4. Ampliar límites: la imagen se hace más pequeña viendose los elementos que no se ven en la imagen original. No aumentan las medidas de la ventana y no se observan las barras de desplazamiento.

5. Ampliar límites manteniendo el marco: igual que el anterior.

En resumen si nos interesa es ampliar la imagen sin alterarla (hacer zoom) nos interesa ampliar el marco (frame) de la vista y mantener los límites (bounds) originales.

¿Y que pasa con el ratón? Pués algo bastante interesante. En todo momento las coordenadas que devuelve son las coordenadas referidas a los límites de la vista.

Más información

Como siempre la mjor fuente de información sobre estos temas de gráficos la puedes encontrar en Apple: View Programming Guide for Cocoa. [http://developer.apple.com/documentation/Cocoa/Conceptual/CocoaViewsGuide/index.html].

Entradas Relacionadas

Zoom 1, dimensionado de la ventana y scroll

5 agosto 2008

Siguiendo con mi aprendizaje en Cocoa me he encontrando intentando hacer zoom en una vista. En principio las instrucciones son sencillas, pero la realidad es bastante más complicada. Hay muchos conceptos sobre las vistas en Cocoa que evidentemente no habia llegado a comprender. Así que esta nueva serie de entradas son un intento de dejar constancia de mi nueva compresión sobre este tema.

Auto dimensionado

El proyecto comienza en primer lugar con una prueba como referencia para posteriores cambios.

Creamos en XCode una vista personalida (File | New File | Objective-C NSView subclass). Luego en Interface Builder creamos la ventana, añadimos una vista personalizada (Custom View) y en el Inspector hacemos que se corresponda con la vista que hemos creado antes. En fin, nada que no haya hecho en otras ocasiones. Simplemente que en este caso he tenido el cuidado de crear una vista de 400×400 y ajustar la ventana de la aplicación en consecuencia.

En segundo lugar en XCode escribimos el código adecuado para dibujar dos cuadrados y dos círculos en la vista

- (void)drawRect:(NSRect)lienzo {
    //Dibujar el fondo de color blanco
	[[NSColor whiteColor] set];
	[NSBezierPath fillRect:lienzo];

    // Cuadrados
    NSRect cuadrado1 = NSMakeRect(0.0, 0.0, 25.0, 25.0);
	[[NSColor blackColor] set];
    NSRectFill(cuadrado1);
    NSRect
    NSRect cuadrado2 = NSMakeRect(350.0, 350.0, 100.0, 100.0);
    NSRectFill(cuadrado2);

    // Círculos
    [[NSColor redColor] set];
    NSBezierPath* circulo1 = [NSBezierPath bezierPath];
    [circulo1 appendBezierPathWithOvalInRect:NSMakeRect(180.0, 180.0, 40.0, 40.0)];
    [circulo1 stroke];

    NSBezierPath* circulo2 = [NSBezierPath bezierPath];
    [circulo2 appendBezierPathWithOvalInRect:NSMakeRect(300.0, 100.0, 200.0, 200.0)];
    [circulo2 stroke];
}

En este caso la prueba consiste en modificar en Interface Builder la propiedad de autodimensionado (autoresizing). En la primera imagen se ve el resultado sin el auto dimensionado: la ventana de la aplicación es más grande, pero el dibujo no cambia.

Vista sin autodimensionado

Vista sin autodimensionado

En la segunda imagen se ve como al activar al autodimensionado si hacemos más grande la ventana de la aplicación, la vista del dibujo se hace más grande. Esto permite observar elementos del dibujo que antes quedaban incompletos.

Vista con autodimensionado

Vista con autodimensionado

 

 

Ventana con scroll

El siguiente elemento necesario para el Zoom es que la ventana de la aplicación sea capaz de mostrar las barras de desplazamiento si la vista es más grande que la ventana. Para ello basta en Interface Builder seleccionar la vista e indicarle que incluya la vista en una vista con desplazamiento (Scroll). Se hace seleccionando la opción de menú Layout | Embed Objects in | Scroll view. Es en esta acción tan sencilla dónde comienzan los problemas. Por que ahora en la ventana de la aplicación hay dos vistas: la vista personalizada que hemos creado y la vista de scroll que controlará el sistema.Y además, ¿no tenian que salir las barras de desplazamiento?

 

Vista con Scroll, sin barras de desplazamiento

Vista con Scroll, sin barras de desplazamiento

Para que funcione tenemos que asegurarnos que la vista personalizada no se redimensione automáticamente.

Autodimensionado sin activar para tener las barras de desplazamiento

Autodimensionado sin activar para tener las barras de desplazamiento

Entonces será cuando al hacer la ventana más pequeña que la vista personalizada que hemos definido se vean las barras de desplazamiento.

Ventana con scroll

Ventana con scroll

Para acabar por ahora unos detalles. Es posible definir que las barras de desplazamiento solo se vean cuando haga falta, para ello en Interface Builder, se selecciona la vista de scroll (Scroll View) y en la pestaña Attributes se pude marcar la opción Automatically Hide Scrollers. Basta hacer la vista de scroll ligeramente más grande que la vista personalizada y solo se veran las barras de desplazamiento cuando se haga más pequeña la ventana.

Todavía queda bastante trabajo por hacer: si nos fijamos en las barras de desplazamiento vemos que los deslizadores quedan en la parte inferior izquierda, centrando el dibujo o la imagen en esa esquina, hará falta algo de programación para arreglarlo. Otro problema es que si ampliamos la ventana, la vista personalizada siempre queda en la esquina inferior izquierda, ignorando la opción escogida en Interface Builder, no estoy seguro de por que pasa, pero creo que es un error de esta versión de XCode (3.0).

En la próxima entrada tengo previsto hablar sobre el marco de la vista, limites y areas visibles, la geometría de la vista.

Entradas Relacionadas

iPhoneSoftware.es

24 julio 2008

Ya he explicado que últimamente la información sobre cocoa y Objective-C en castellano ha aumentado mucho gracias a toda la gente que se interesa por la programación en el iPhone. Esta página es un buen ejemplo de ello.

En estos momentos tiene mucha actividad y dos apartados que recomiendo especialmente: La antesala al desarrollo, dónde explican muchas cosas sobre Cocoa y XCode que yo debería de explicar si no fuera tan perezoso. Y Tutoriales para aprender desde cero, unas explicaciones detalladas para comenzar a programar el iPhone y su hermanastro iTouch.

Distancia punto a segmento, quinta parte y final

10 julio 2008

Por fin el final de esta serie de entradas sobre la distancia de un punto a un segmento. La verdad es que me está costando mucho más la preparación del programa y su documentación que el propio programa. Sospecho que esto de documentar un programa es la parte más complicada de todo. En fin, veamos como se inicializa la ventana, como se dibuja y como se controla el ratón.

Inicializando la vista

- (id)initWithFrame:(NSRect)frame {
    self = [super initWithFrame:frame];
    if (self) {
        // Inicializa el marco de la vista.
        NSRect	bounds = [self bounds];
        // Cálcula la escala de la ventana
        // Para el ejemplo se divide la pantalla en 10 divisones horizontales
        // La escala vertical es igual a la horizontal
        escalaX = bounds.size.width/10.0;
        escalaY = bounds.size.height/8.0;
        //El desplazamiento respecto al origen de la ventana
        desplazamientoX= bounds.size.width/2.0;
        desplazamientoY= bounds.size.height/2.0;

        // Establece el valor inicial del punto C
        C = NSMakePoint(2.0, 2.0);
    }
    return self;
}

En la inicialización de la vista se calcula la escala y el desplazamiento de la vista respecto a las coordenadas cartesianas que utilizarán los puntos (ver la entrada anterior, cuestiones de escala y origenes). Para simplificar el programa el número de divisiones y el desplazamiento se calculan a partir de constantes: 10 divisiones horizontales, 8 divisiones verticales y el origen de coordenadas en el centro de la vista. Igualmente el tamaño de la ventana y la vista esta fijados. Espero analizar más adelante la posibilidad de cambiar escalas y origenes en otro programa.

En la misma el mismo método se da un valor inicial al punto C externo al segmento. El valor asignado es, en este caso, totalmente arbitrario. Se ha escogido por que me parecía bien que quedara en esa posición bien diferenciado del segmento.

Dibujo de la vista

// Dibujar la vista
- (void)drawRect:(NSRect)rect {
    // Fondo blanco
    NSRect	bounds = [self bounds];
	[[NSColor whiteColor] set];
	NSRectFill(bounds);

    // Rejilla
    [[NSColor blackColor] set];
    int i,j;
    for (i=0;i<=8;i++){
        for(j=0;j<=10;j++){
            NSPoint punto = NSMakePoint(escalaX*j,escalaY*i);
            NSRect cuadrado = NSMakeRect(punto.x-1.0, punto.y-1.0, 2.0, 2.0);
            NSRectFill(cuadrado);
            }
    }

    // Ejes
    float ancho = bounds.size.width;
    float alto = bounds.size.height;
    [NSBezierPath strokeLineFromPoint:NSMakePoint(0.0,desplazamientoY) toPoint:NSMakePoint(ancho, desplazamientoY)];
    [NSBezierPath strokeLineFromPoint:NSMakePoint(desplazamientoX, 0.0) toPoint:NSMakePoint(desplazamientoX, alto)];

    // Segmento AB
	[[NSColor blueColor] set];
    NSPoint punto1 = [self escalar:[segment A]];
    NSRect cuadrado = NSMakeRect(punto1.x-2.0, punto1.y-2.0, 4.0, 4.0);
    NSRectFill(cuadrado);
    NSPoint punto2 = [self escalar:[segment B]];;
    cuadrado = NSMakeRect(punto2.x-2.0, punto2.y-2.0, 4.0, 4.0);
    NSRectFill(cuadrado);
    [[NSColor greenColor] set];
    [NSBezierPath strokeLineFromPoint:punto1 toPoint:punto2];

    // Punto C
    NSPoint punto3 = [self escalar:C];
    [[NSColor redColor] set];
    cuadrado = NSMakeRect(punto3.x-2.0, punto3.y-2.0, 4.0, 4.0);
    NSRectFill(cuadrado);

    // Distancia de punto a segmento
    [muestraDistancia setFloatValue:[segment distanciaPunto:C]];
}

Aunque no deberia ser difícil de entender hay varios detalles interesantes. Si no tienes claro las diferentes formas de dibujo con Cocoa seria conveniente que las revisaras antes de seguir con este ejemplo.

En primer lugar se dibuja el fondo blanco. Luego la rejilla, cómo no es posible dibujar puntos se dibujan pequeños cuadrados que haran el mismo papel. En tercer lugar se dibuja el segmento. Se puede ver como antes de dibujar el punto se escala a la medida de la vista:

NSPoint punto1 = [self escalar:[segment A]];

En cuarto lugar se dibuja el punto C (igualmente como cuadrado) y se acaba calculando la distancia entre punto y segmento, enviando el adecuado mensaje al objeto [segment distanciaPunto:C] y mostrandolo en pantalla.

Moviendo puntos con el ratón

Para poder mover los puntos con el ratón he optado por hacerlo a partir del arrastre del ratón. Para ello hay que poner el codigo adecuado en el método mouseDragged: Este método proporciona las coordenadas del punto donde se ha producido el arrastre del ratón.

// Analiza el arrastre del ratón
- (void)mouseDragged:(NSEvent *)theEvent {
    // Obtenemos las coordenadas de punto donde se ha hecho el desplazacmiento
	NSPoint	eventLocation = [theEvent locationInWindow];
    NSPoint impacto = [self convertPoint:eventLocation fromView:nil];

    // Puntos a comprobar, se escalan a las dimensiones de la vista
    NSPoint puntoA = [self escalar:[segment A]];
    NSPoint puntoB = [self escalar:[segment B]];
    NSPoint puntoC = [self escalar:C];

    // Resultado del arrastre
    NSPoint punto  = [self reducir:impacto];

    // Se comprueba por orden  si el punto de impacto està cercano
    // a uno de los puntos (extremos A i B del segmento o punto C)
    // si es así se asignan las nuevas coordenadas
    float d = [segment distancia:impacto a: puntoA];
    if (d<=30)
        [segment establecerA:punto];
    else{
        d = [segment distancia:impacto a: puntoB];
        if (d<=30)
            [segment establecerB:punto];
        else{
            d = [segment distancia:impacto a:puntoC];
            if (d<=30)
                C = punto;
        }
    }
    [self setNeedsDisplay:YES];
}

La primera parte permite obtener las coordenadas del punto donde esta ratón al arrastrarlo convertidas a las coordenadas de la vista.Hay que recordar que por defecto el evento da las coordenadas respecto a las coordenadas de la ventana que recibe el evento.

Luego se recuperan los punto que forman el segmento y el punto C convertidos a las coordenadas de la ventana.

En un paso previo se calcula el posible resultado al convertir las coordenadas del punto de arrastre a coordenadas cartesianas. Esto se hace para no tener que hacer el cálculo dentro de cada uno de las posibles opciones.

La parte importante es la selección de cual de los tres punto es el que va a ser arrastado. Esto se ha hecho a partir de sentencia alternativas if. Calculando la distancia entre punto y comprobando que es menor que un valor arbitrario. Este valor arbitrario o sensibilidad (30) lo he escogido por que funciona y en programas más complejos podría ser una parametro a controlar en las preferencias del programa.

Y acaba estableciendo la necesidad de rehacer el dibujo.

Espero que si alguien ha seguido este ejemplo haya aprendido algo de todo lo que yo he aprendido haciendolo y explicandolo.

Hasta otra.

Código fuente del ejemplo

distancia.zip

 

Archivos alojados en Dropbox.com

Entradas relacionadas

Distancia punto a segmento, cuarta parte

9 julio 2008

Cuestiones de escala y origenes

En anteriores entradas ya he explicado la teoria del cálculo de de la distancia de un punto a un segmento, también he mostrado un ejemplo de una clase escrita con Objective-C que realiza este cálculo y luego he comenzado a explicar un programa que utiliza esta clase. En este ejemplo los puntos que marcan los extremos del segmento y el punto externo al segmento solamente podrán tener valores enteros y además limitados a valores pequeños (entre -10 y 10).

Para poder representar adecuadamente estos punto en una vista hace falta “traducir” las coordenadas de los puntos a coordenadas en la pantalla. Esto es lo que intentaré mostrar en esta entrada.

Coordenadas cartesianas

La representación habitual de puntos, vectores, rectas y otros elementos geometricos se hace utilizando las coordenadas cartesianas. A cada punto del plano se le hace corresponder un par de valores (x, y) que definen su posición respecto a los ejes coordenados. De cara a la programación hace falta notar que las estas medidas no son absolutas. Simplemente se pide que la distancia entre cada una de la marcas de los ejes sean iguales y a partir de esta distancia se calculan las coordenadas. Cuando programemos nuestro sistema haba que traducir estas distancias a valores absolutos, normalmente en píxels.

Coordenadas en una ventana Cocoa

Para indicar las coordenadas de un punto en una ventana Cocoa el sistema que se utiliza esta basado en el sistema anterior, con un par de diferencias muy significativas:

  1. El origen de coordenadas esta en la esquina inferior izquierda de la ventana
  2. Las medidas son en puntos

Esto último representa un cambio respecto a la forma de trabajar habitual en informática. De forma tradicional la medida más utilizada es un píxel, el elemento más pequeño que se puede represantar en la pantalla del ordenador. Mac OS X desde el principio comenzo a cambiar la forma de trabajar al utilizar Quartz para dibujar en pantalla. En este sistema basado en PDF el sistema no guarda los píxels que componen una ventana, un recta o cualquier otro elemento, sino que los guarda de forma vectorial. Esto permite que lo que se muestra por pantalla sea fácilmente trasladable a la impresora (o a cualquier otro dispositivo de salida) y ciertas virguerias visuales muy propias de OS X.

Este “punto” ha respetado la tradicional definición de 1/72 de pulgada, con lo cual el sistema gráfico de OS X se ha de encargar de calcular los píxeles necesarios para llenar el “punto” segun la resolución del dispositivo de salida. No solo eso, sino que además a partir de OS X 10.5 Leopard es posible que el usuario cambie la escala con la cual se representa el punto. Este cambio todavía no esta muy presente en el sistema y se espera que al hacer las pantallas más grandes cada vez sea más utilizado en el futuro.

A efectos prácticos de programar esto significa que tendremos que conocer muy bien las opciones que Cocoa nos ofrece para calcular puntos y coordenadas. Olvidandonos de los píxels y dejando su gestión al sistema. Puedes obtener más información en: Cocoa Drawing Guide: Coordinate Systems Basics.

Escala y desplazamiento

Así pués vistas las diferencias entre los dos sistemas de coordenadas hay dos cosas por hacer, la primera es convertir las medidas cartesianas a los puntos de las ventanas Cocoa (y al contrario) con un factor de escala. Lo segundo es trasladar el origen de coordenadas utilizando un desplazamiento.

Matemáticamente para convertir un punto en coordenadas cartesianas a las coordenadas de la ventana hay que hacer el siguiente cálculo:

Xv = Xc * escalaX + desplazamientoX;

Donde Xv es la coordenada cartesiana y Xv la coordenada de la ventana. Para la coordenada Y el cálculo es el equivalente.

Para hacer el camino contrario, de coordenadas de la ventana a coordenadas cartesianas seria:

Xc = (Xv – desplazamientoY)/escalaX;

Un detalle importante es dar los valores adecuados al factor de escala (escalaX) y al desplazamiento (desplazamientoX). En el programa de ejemplo los he calculado a partir de las medidas de la ventana y el número de divisiones cartesianas que queria mostrar. En un programa más complejo esto nos permitiria crear una herramienta de zoom y centrar la vista en un punto concreto marcado por el ratón, por ejemplo.

También importante observar que la escala en las coordenadas horizontales no tiene por que ser la misma que en las verticales; pero que es recomendable que sean iguales para no deformar los objetos.

Código de ejemplo

En el programa de ejemplo la vista personalizada SVista contiene las variables y funciones necesarias para hacer la conversión entre los puntos. La Interface quedará:

#import <Cocoa/Cocoa.h>
#import "segmento.h"

@interface SVista : NSView {
   IBOutlet Segmento *segment;         // El segmento, se inicializara en IB
   IBOutlet id muestraDistancia;       // Muestra la distancia calculada en la ventana
   NSPoint C;                          // Punto externo al segmento
   float escalaX;                      // escala de la rejilla
   float escalaY;
   float desplazamientoX;
   float desplazamientoY;
}
- (NSPoint)escalar:(NSPoint)punto;
- (NSPoint)reducir:(NSPoint)punto;
@end

Cómo puede verse se han definido las variables para las escalas y los desplazamientos horizontales. Igualmente estan definidas las funciones que nos permiten convertir los puntos a unas coordenadas u otras según nuestro interés.

En la implementación de SVista.m estarán las funciones:

// Convierte el punto a la escala de la vista
-(NSPoint)escalar:(NSPoint)punto{
    NSPoint p;
    p.x = punto.x*escalaX+desplazamientoX;
    p.y = punto.y*escalaY+desplazamientoY;
    return p;
}

// Convierte de la escala de la vista a natural
-(NSPoint)reducir:(NSPoint)punto{
    NSPoint p;
    p.x = (punto.x-desplazamientoX)/escalaX;
    p.y = (punto.y-desplazamientoY)/escalaY;
    return p;
}

Redondeo

Para que el ejemplo fuera más sencillo de observar, interesa que los puntos que forman el segmento y el punto exterior solo pudieran tener valores enteros. En este caso al desplazar un punto habrá que aproximarlo al entero más cercano. Para ello se puede utilizar la función floor() que es estándar de C incluida en la librería matemática “math.h“. Un pequeño detalle es que la funciona floor() no redondea, solamente da el entero inferior al valor introducido, para hacer el redondeo hay que sumar el valor 0,5 al valor que nos intesesa redondear.

De esta forma el código queda:

// Convierte el punto a la escala de la vista
-(NSPoint)escalar:(NSPoint)punto{
    NSPoint p;
    p.x = punto.x*escalaX+desplazamientoX;
    p.y = punto.y*escalaY+desplazamientoY;
    return p;
}

// Convierte de la escala de la vista a natural
-(NSPoint)reducir:(NSPoint)punto{
    NSPoint p;
    // floor calcula el entero inferior al punto calculado, para redondear hay que
    // sumar 0.5
    p.x = floor((punto.x-desplazamientoX)/escalaX+0.5);
    p.y = floor((punto.y-desplazamientoY)/escalaY+0.5);
    return p;
}

Y esto es todo por ahora, quedan pendientes de explicar los “pequeños” detalles del dibujo de los puntos y cómo mover los puntos utilizando el ratón. Pero todo llegará a su hora.

Entradas Relacionadas

Distancia punto a segmento, tercera parte

8 julio 2008

En esta tercera parte sobre la distancia de un punto a un segmento comienzo a construir un ejemplo de utilización.

Como el ejemplo es bastate complejo, los impacientes pueden saltarse las explicaciones y descargar el código fuente del ejemplo en: distancia.zip.

En principio el ejemplo iba a ser mucho más sencillo, pero me entro la vena didáctica y acabe haciendo que los puntos del segmento y del punto exterior fueran números enteros y que el origen de coordenadas estuviera en el centro de la ventana. Esto ha significado añadir la “traducción” de los puntos de la vista a coordenadas cartesianas.

La ventana y los objetos del programa

En este caso la definición de la ventana comienza en Xcode, una vez creado el projecto hay que crear nuevas clases: una corresponde a la definición del segmento con el codigo explicado anteriormente (distancia punto a segmento, segunda parte), y otra correspondiente a la vista personalizada (NSView) que dibujará los objetos.

En la vista personalizada definiremos dos conectores (IBOutlet), uno para mostrar la distancia y otro para el segmento:

#import <Cocoa/Cocoa.h>
#import "segmento.h"
@interface SVista : NSView {
    IBOutlet Segmento *segment;         // El segmento, se inicializara en IB
    IBOutlet id muestraDistancia;       // Muestra la distancia calculada en la ventana
}
@end

Seguidamente hay que ir a Interface Builder para definir la ventana del programa. En la ventana del programa añadimos una vista personalizada (Custom View) que podemos encontrar en la libreria de objetos Cocoa. Para simplificar el ejemplo en este caso las medidas de la ventana son fijas. Una vez colocada la ventana, en el apartado “Class Identity ” del inspector hay que indicar que la clase personalizada será la que hemos creado con Xcode. En la siguiente imagen puede verse la ventana y los conectores (outlets) que hemos definido antes.

Después hay que añadir una instancia de segmento. En Xcode 3 ha cambiado el comportamiento respecto a anteriores versiones. En esta versión si añadimos una instancia de una clase a Interface Builder el sistema se encargará de la gestión del objeto (inicializar, destruir, gestionar la memoria). Aunque siempre es posible utilizar el método anterior.

Para ello hace falta añadir un NSObjet de la libreria de objetos (simplemente arrastrando) y en el inspector seleccionar la clase del objeto.

En la misma ventana de la aplicación además de la vista personalizada podemos añadir una etiqueta (Label, NSTextField) que permitirá mostrar la distancia.

El último paso en IB es conectar los conectores al elemento correspondiente. Esto se hace con apretando la tecla de control y arrastrando el ratón desde el objeto a conectar al conectado. En este caso desde la vista a la instancia de la clase Segmento.

De momento esto es todo por hoy, espero seguir explicando cosas sobre como se dibuja la ventana, como se escalan los puntos a la ventana y como se hace el arrastre de los puntos con el ratón.

Código fuente del ejemplo

Archivos alojados en Dropbox.com

Entradas relacionadas