Image viewer (JavaScript) A little bit more than a simple "Hello world" application - write an image viewer in GTK+. Includes an introduction to the JavaScript language. Jonh Wendell jwendell@gnome.org Johannes Schmid jhs@gnome.org Marta Maria Casetti mmcasettii@gmail.com 2013 Fran Dieguez frandieguez@gnome.org 2012-2013. Image viewer

Neste titorial, imos escribir un aplicativo GTK moi sinxelo que carga e mostra un ficheiro de imaxe. Vostede aprenderá a:

Escribir unha interface de usuario GTK básica en JavaScript

Xestionar eventos conectando sinais a xestores de sinais

Dispoñer interfaces de usuario GTK usando contedores

Cargar e mostrar ficheiros de imaxe

You'll need the following to be able to follow this tutorial:

An installed copy of the Anjuta IDE

Unha copia instalada do intérprete gjs

Coñecemento básico dunha linguaxe de programación orientada a obxectos

Cree un proxecto de Anjuta

Antes de comezar a programar, deberá configurar un proxecto novo en Anjuta. Isto creará todos os ficheiros que precise para construír e executar o código máis adiante. Tamén é útil para manter todo ordenado.

Inicie Anjuta e prema FicheiroNovoProxecto para abrir o asistente de proxectos.

Seleccione JavaScript xenérico desde a lapela JS, prema Adiante e complete os seus detalles nas seguintes páxinas. Use image-viewer como nome do proxecto e cartafol.

Prema Aplicar e o proxecto será creado. Abra src/main.js desde a lapela Proxecto ou Ficheiros. Contén código de exemplo moi básico.

Primeiros pasos en JavaScript: Ola mundo

Antes de comezar a escribir o visor de imaxes faremos unha introdución á forma na que JavaScript se usa en GNOME. Por suposto, o primeiro contacto con calquera linguaxe de programación debería ser un programa Ola Mundo que pode atopar main.js:

print ("Ola mundo!");

Isto debería ser natural se está familiarizado con calquera outra linguaxe de programación. A función printi chámase co argumento «Ola Mundo», que se imprimirá en pantalla. Teña en conta que cada liña de código remata con un punto e coma.

Clases en JavaScript

Esta é a forma estándar de definir unha clase en JavaScript:

Isto define unha clase chamada MyClass. Vexa cada unha das partes da definición da clase:

function MyClass is the constructor of the class — its name must match the class's name. You can access any member of the class by using the this object; here, the constructor calls the class's _init method.

The MyClass.prototype block is where you define the structure of the class. Each class is made up of methods (functions) and fields (variables); there are three methods and two fields in this example.

The first method defined here is called _init, and we specify that it is a function with no arguments:

_init: function ()

We write the function inside some curly braces. Two fields are defined here, propertyA and propertyB. The first is set to a string and the second is set to an integer (10). The function doesn't return any value.

The next method is called aMethod and has two arguments, which it prints out when you call it. The final method is dumpProperties, and prints the fields propertyA and propertyB.

Teña en conta como se ordena a definición da clase (prototipo); cada definición de función sepárase con unha coma.

Now that MyClass has been defined, we can play with it:

This code creates a new instance of the class called o, runs aMethod, changes propertyA to a different string, and then calls dumpProperties (which outputs the fields).

Save the code in the main.js and then run it by using RunExecute from the menu or using the toolbar.

Un primeiro aplicativo en GTK

Let's see what a very basic Gtk application looks like in JavaScript:

Botemos unha ollada ao que está pasando:

The first line imports the Gtk namespace (that is, it includes the Gtk library). The libraries are provided by GObject Introspection (gi), which provides language bindings for many GNOME libraries.

Gtk.init initializes the Gtk library; this statement is mandatory for all Gtk programs.

The next line creates the main window by creating a new Gtk.Window object. You can pass several properties to the window's constructor by using the syntax {property: value, property: value, ...}. In this case we are setting the title of the window.

The next line explicitly shows the window. In Gtk, every widget is hidden by default.

Finally, Gtk.main runs the main loop — in other words, it executes the program. The main loop listens for events (signals) from the user interface and then calls a signal handler which will do something useful. We'll learn more about signals shortly.

Save the code in main.js and run it. You will notice that the application does not quit when you close the window. This is because we haven't set up a signal handler to deal with the window's destroy (close) signal yet. We'll do this shortly, but for now you can just hit CtrlC in the terminal window to quit the program.

Engadir clases

The proper way of doing Gtk programming is by using classes. Let's rewrite the simple code you just wrote using classes:

Notice that the program is the same; we just moved the window creation code to our own ImageViewer class. The class's constructor calls the _init method, which creates and shows the window. We then create an instance of the class before running the main loop (Gtk.main).

This code is modular and can be split into multiple files easily. This makes it cleaner and easier to read.

Sinais

Signals are one of the key concepts in Gtk programming. Whenever something happens to an object, it emits a signal; for example, when a button is clicked it gives off the clicked signal. If you want your program to do something when that event occurs, you must connect a function (a "signal handler") to that signal. Here's an example:

The last two lines create a Gtk.Button called b and connect its clicked signal to the button_clicked function, which is defined above. Every time the button is clicked, the code in the button_clicked function will be executed. It just prints a message here.

A sintaxe para conectar calquera sinal a unha función é:

, );]]>

You can find signal definitions for any object in the GTK class reference.

Vostede pode simplificar o código facendo uso da definición en liña de funcións:

Pechar a xanela

Ao pechar unha xanela Gtk realmente non se pecha, agóchase. Isto permítelle manter unha xanela (útil se quere preguntarlle ao usuario se realmente desexa pechar a xanela, por exemplo).

No noso caso, queremos pechar a xanela. A forma máis doada de facer isto é conectar o sinal hide do obxecto GtkWindow a unha función que peche o aplicativo. Vaia de novo ao ficheiro image-viewer.js e engada o seguinte código ao método _init, na liña superior a this.window.show.

this.window.connect ("hide", Gtk.main_quit);

This connects the hide signal of the window to Gtk's main_quit function, which ends the execution of the Gtk main loop. Once the main loop finishes, the function Gtk.main returns. Our program would continue to run any code written after the Gtk.main (); line, but since we don't have any code after that point, the program just ends.

Contedores: deseñar a interface de usuario

Widgets (controls, such as buttons and labels) can be arranged in the window by making use of containers. You can organize the layout by mixing different types of containers, like boxes and grids.

A Gtk.Window is itself a type of container, but you can only put one widget directly into it. We would like to have two widgets, an image and a button, so we must put a "higher-capacity" container inside the window to hold the other widgets. A number of container types are available, but we will use a Gtk.Box here. A Gtk.Box can hold several widgets, organized horizontally or vertically. You can do more complicated layouts by putting several boxes inside another box and so on.

There is a graphical user interface designer called Glade integrated in Anjuta which makes UI design really easy. For this simple example, however, we will code everything manually.

Let's add the box and widgets to the window. Insert the following code into the _init method, immediately above the this.window.show line:

The first line creates a Gtk.Box called main_box and sets two of its properties: the orientation is set to vertical (so widgets are arranged in a column), and the spacing between the widgets is set to 0 pixels. The next line then adds the newly-created Gtk.Box to the window.

So far the window only contains an empty Gtk.Box, and if you run the program now you will see no changes at all (the Gtk.Box is a transparent container, so you can't see that it's there).

Empaquetado: Engadir widgets ao contedor

To add some widgets to the Gtk.Box, insert the following code directly below the this.window.add (main_box) line:

The first line creates a new Gtk.Image called image, which will be used to display an image file. Then, the image widget is added (packed) into the main_box container using Gtk.Box's pack_start method.

pack_start takes 4 arguments: the widget that is to be added to the Gtk.Box (child); whether the Gtk.Box should grow larger when the new widget is added (expand); whether the new widget should take up all of the extra space created if the Gtk.Box gets bigger (fill); and how much space there should be, in pixels, between the widget and its neighbors inside the Gtk.Box (padding).

Gtk containers (and widgets) dynamically expand to fill the available space, if you let them. You don't position widgets by giving them a precise x,y-coordinate location in the window; rather, they are positioned relative to one another. This makes handling window resizing much easier, and widgets should automatically take a sensible size in most situations.

Also note how the widgets are organized in a hierarchy. Once packed in the Gtk.Box, the Gtk.Image is considered a child of the Gtk.Box. This allows you to treat all of the children of a widget as a group; for example, you could hide the Gtk.Box, which would also hide all of its children at the same time.

Agora escriba estas dúas liñas, embaixo das dúas que acaba de engadir:

These lines are similar to the first two, but this time they create a Gtk.Button and add it to main_box. Notice that we are setting the expand argument (the second one) to false here, whereas it was set to true for the Gtk.Image. This will cause the image to take up all available space and the button to take only the space it needs. When you maximize the window, the button size will remain the same, but the image size will increase, taking up all of the rest of the window.

Finally, we must change the this.window.show (); line to read:

this.window.show_all ();

This will show the child of the Gtk window, and all of its children, and its children's children, and so on. (Remember that Gtk widgets are all hidden by default.)

Loading the image: Connecting to the button's <code>clicked</code> signal

When the user clicks on the Open button, a dialog should appear so that the user can choose a picture. Once chosen, the picture should be loaded and shown in the image widget.

The first step is to connect the clicked signal of the button to a signal handler function, which we call _openClicked. Put this code immediately after the var open_button = new Gtk.Button line where the button was created:

We are using the Lang JavaScript helper here. It allows us to connect a class method to the signal, rather than a plain function (without a class) which we had used before for the window's hide signal. Don't worry about this for now, it's just a technical detail. For it to work, you also need to put the following line at the top of the file:

const Lang = imports.lang;
Loading the image: Writing the signal's callback

Now we can create the _openClicked() method. Insert the following into the ImageViewer.prototype code block, after the _init method (and not forgetting the comma):

This is a bit more complicated than anything we've attempted so far, so let's break it down:

The line beginning with var chooser creates an Open dialog, which the user can use to choose files. We set four properties: the title of the dialog; the action (type) of the dialog (it's an "open" dialog, but we could have used SAVE if the intention was to save a file; transient_for, which sets the parent window of the dialog; and modal which, if set to true, prevents the user from clicking on another area of the application until the dialog is closed.

The next two lines add Cancel and Open buttons to the dialog. The second argument of the add_button method is the (integer) value that is returned when the button is pressed: 0 for Cancel and 1 for Open.

Notice that we are using stock button names from Gtk, instead of manually typing "Cancel" or "Open". The advantage of using stock names is that the button labels will already be translated into the user's language.

set_default_response determines the button that will be activated if the user double-clicks a file or presses Enter. In our case, we are using the Open button as default (which has the value 1).

The next three lines restrict the Open dialog to only display files which can be opened by Gtk.Image. A filter object is created first; we then add all kinds of files supported by Gdk.Pixbuf (which includes most image formats like PNG and JPEG) to the filter. Finally, we set this filter to be the Open dialog's filter.

chooser.run displays the Open dialog. The dialog will wait for the user to choose an image; when they do, chooser.run will return the value 1 (it would return 0 if the user clicked Cancel). The if statement tests for this.

Assuming that the user did click Open, the next line sets the file property of the Gtk.Image to the filename of the image selected by the user. The Gtk.Image will then load and display the chosen image.

In the final line of this method, we destroy the Open dialog because we don't need it any more.

Executar o aplicativo

All of the code you need should now be in place, so try running the code. That should be it; a fully-functioning image viewer (and a whistlestop tour of JavaScript and Gtk) in not much time at all!

Implementación de referencia

Se ten problemas con este titorial, compare o seu código con este código de referencia.

Seguintes pasos

Aquí hai algunhas ideas sobre como pode estender esta sinxela demostración:

Have the user select a directory rather than a file, and provide controls to cycle through all of the images in a directory.

Aplicar filtros aleatorios e efectos á imaxe cando se carga e permitir ao usuario gardar a imaxe modificada.

GEGL fornece capacidades moi potentes de manipulación de imaxes.

Permitir ao usuario cargar imaxes desde recursos de rede compartidos, escáneres e outras fontes máis complicadas.

You can use GIO to handle network file transfers and the like, and GNOME Scan to handle scanning.