KDEによるGUI作成

Richard J. Moore

日本語訳: 菰田泰生 19990618

導入

KDEでGUIを作ることはたくさんの既存の利用可能なGUIコンポーネントや、開発者のためのQtが提供するフレームワークのおかげでとても簡単にできます。不幸にも大量のパブリックドメインのソースコード(例えばKDE全体)や詳細なクラスドキュメントのせいで、GUIを作るためにどうやって物を組み合わせていくかを説明するチュートリアルがありませんでした。この文書の目的はそのような文書を提供することです。具体的に議論するために`ドラッグアンドドロップ'、URLサポート、オンラインヘルプのような機能を含むとても簡単なKDEアプリケーションを作っていきたいと思います。この文書はC++や基本的なコンパイルの問題については説明しません。どうやってGUIを組み合わせるかだけを説明します。

始めるにあたって1つのメニューを含むメニューバーだけから構成されるとても簡単なGUIを作ってみましょう。`Exit'コマンドだけをメニューに含みます。このむしろ謙虚なものから、私たちはアプリケーションをシンプルながら機能的なエディタに拡張していきたいと思います。これはチュートリアルにしては手ごわいものかも知れませんが、QtやKDEによって提供されているコンポーネントを再利用することを見ることで簡単に練習できます。私は最初にコードを見せてあとで詳しく追っていきます。全てのサンプルコードはKDEの開発者用ウェブサイトのhttp://developer.kde.org/examples.tar.gzから自由にダウンロードできます。

最小のアプリケーション

私たちはこれから最小のKDEアプリケーションについて詳細に見ていきたいと思います。それは`Exit'コマンドだけを提供しますが、すでにフローティングメニューバー、セッションマネージメント、国際化もサポートしています。
 
#ifndef EDIT_H
#define EDIT_H

#include <ktopwidget.h>

class Edit : public KTopLevelWidget
{
  Q_OBJECT

public:
  Edit();

  ~Edit();

public slots:
 void commandCallback(int id_);

private:
  // 子ウィジェット
  KMenuBar *menu;
};
#endif // EDIT_H
Edit クラス宣言 
 最初に見るべきなのはクラス宣言で、Editクラスとしてアプリケーションにインプリメントされているのが分かるでしょう。
Editは全てのKDEアプリケーションの基本クラスであるKTopLevelWidget (KTLW)の派生クラスです。KTLWはアプリケーションのメニューバー、ステータスバー、ツールバー、セッションマネージメントを管理します。次にクラス宣言にあるのは`Q_OBJECT'マクロです。これは重要なQtの特徴である"シグナル"と"スロット"のサポートのために必要です。Editクラスのただ一つのスロットがcommandCallback()メソッドで、`public slots:'のようにパブリックアクセスのスロットとして宣言されています。スロットは本質的にタイプセーフなコールバックメソッドです。オブジェクトによって、「Editクラスは connect()メソッドで使われると表現されているもの」として呼ばれます。残りのクラス宣言は他のC++クラスと同様です。
 
#include <kapp.h>
#include <kmenubar.h>
#include "edit.h"

const int ID_EXIT= 111;

Edit::Edit()
{
  QPopupMenu *file = new QPopupMenu;

  file->insertItem(klocale->translate("Exit"), ID_EXIT);

  menu = new KMenuBar( this );
  CHECK_PTR( menu );
  menu->insertItem( klocale->translate("File"), file );
  menu->show();
  setMenu(menu);

  // 互いを接続します
  connect (file, SIGNAL (activated (int)), SLOT (commandCallback (int)));
}

Edit::~Edit()
{

}

void Edit::commandCallback(int id_)
{
  switch(id_) {
  case ID_EXIT:
    exit(0);
    break;
  }
}

#include "edit.moc"
Edit クラス インプリメント

私たちは今、コンストラクタを最初に見ることによりEditクラスのインプリメントを学びます。コンストラクタはQPopupMenuオブジェクトを生成します、これはFileメニューです。私たちはinsertItem() メソッドを使って`Exit'アイテムを追加します。最初のパラメターはメニューで表示される文字列で、2番目は整数のidです(あなたはクラスドキュメンテーションを見ることで、クラスの全てのメソッドを見つけることができます)。翻訳のためのアプレットの準備としてただの'Exit'文字列のラベルをセットする代わりにklocale->translate() を呼びます。ユーザーが見るであろうどんな文字列もこうされるべきです。私たちは私たち独自のKLocaleオブジェクトを作る必要はありません、というのはKApplication(全てのKDEアプリケーションオブジェクトの基本クラス)が作ってくれるからです。私たちはkapp.hによって提供されるklocaleマクロを使ってKLocateオブジェクトにアクセスします。

次に私たちがやることは私たちのメニューを含むメニューバーを作ることです、私たちはKMenuBarウィジェットを使ってこれをやります。CHECK_PTRマクロ(ウィジェットがうまく作られたかを確かめる)はQtが供給するパラメターが0かどうかをチェックするマクロです(もしパラメターが0ならアプリケーションをエラーともに終了させます)。私たちは私たちがメニューエントリを作ったのとほぼ同じ方法で'File'アイテムを作ります(この一致は驚くに足りません、なぜならQPopupMenuとKMenuBarは同じ基本クラスQMenuDataを継承しているからです)。再び私たちはKLocaleを呼ぶ必要があります、というのは'File'という文字列はユーザーに見えるからです。私たちはshow() メソッドを使ってメニューを表示し、KTLWにsetMenu()でメニューを管理することを知らせます。

私たちのアプレットが動くようにするための最後のステージは、メニューバーエントリをアプリケーションが終了するためのコードと接続することです、これはconnect() 呼び出しによって行なわれます。Qtアプリケーション(つまりKDEアプリケーション)はウィジェットを繋げるためにシグナルとスロットを使います。ウィジェット(もしくは他のあらゆるQObject)は状態が変わった時はいつでもシグナルを発行します。私たちのアプレットではQPopupMenuがユーザーがメニューアイテムを選択した時に発行する activated() シグナルを使用します。このシグナルはEdit クラスで宣言された commandCallback() スロットに接続しなければなりません。 commandCallback() を見て分かるように、スロットは通常のメソッドと違いはそうありません。唯一の違いは宣言で、このメソッドはヘッダーファイルにあるスロットです。commandCallback() は渡されたパラメターが定数 ID_EXIT--私たちが'Exit'メニューアイテムを識別するのと同じ定数--だったらプログラムを終了します。シグナルとスロットはパラメターを持て、それらは他のメソッドのパラメターと同様に扱えます。Qtはシグナルのtype signatureがスロットのそれとマッチするか(そしてマッチしなければエラーを返します)をチェックします。さて、Editコンストラクタのconnect() メソッド呼び出しのところを見てみましょう。
 
  connect (file, SIGNAL (activated (int)), SLOT (commandCallback (int)));
シグナルをスロットに接続

最初のパラメターはシグナルを排除するオブジェクトで、私たちのケースでは QPopupMenu file です。次のパラメターは接続されるべきシグナルです。あなたはシグナルの名前とそのパラメターの型をSIGNAL()マクロを使って特定しなくてはなりません(というのはパラメターはクラスヘッダーファイルかクラスドキュメントに現れるからです)。connect() が取る最後のパラメターはSLOT() マクロを使って特定されるスロットで、前と同様そのパラメターも特定します。Qtは実行時に、もし接続が無効だと知らせます。QObjectはたくさんのオーバーロードバージョンのconnect() メソッドを提供します、詳しくはQt クラスドキュメントを御覧下さい。ひとつのシグナルをひとつ以上のスロットに接続することや、ひとつ以上のシグナルをひとつのスロットに接続することは全く問題はありません。

この簡単なアプレットで注意しておかなければならない最後の特徴はクラスインプリメントの最後の方でインクルードしたedit.mocファイルです。`moc'ファイルはmocプリプロセッサーの出力で、QObjectを継承するどんなクラスにも適用されなければいけません。あなたは通常直接mocを呼び出すよりmakefile経由で呼び出すでしょう。そしてその出力はあなたのアプリケーションにコンパイル、リンクされることを確かめておいて下さい。これを確かめるもっとも簡単な方法は単純にmoc出力をここで使ったクラスインプリメントのアプローチのようにインクルードすることです。あなたがクラス宣言の部分を変更した時にmocが再実行されることを確かめておくことは重要です、というのはそうでないなら変なエラーが出てしまうからです。

あなたはこんな全く使えないアプレットを作るには多過ぎる作業だと考えたでしょう、しかし私たちは長い道のりを来ました。私たちはセッションマネージメント、メニュー作成、国際化、Qtの基礎シグナルとスロットを学びました。次のステージでは私たちのアプレットを実際に何かをさせるようにします!

機能を追加する

今私たちは基礎を知っているのでもうちょっと速く進めるでしょう。私はさっきのセクションでやったように詳しくコードを追うようなことはしません。私たちの目的はアプレットに何かをさせるようにすることで、ここでは文章を編集したいと思います。

あなたはこのチュートリアルのソースファイルのexample2でこのバージョンの Edit の完全なコードが見れます。

Qtはテキストを編集するウィジェット、QMultiLineEditを提供します。これは私たちのエディタアプレットの核をなします。私たちはEdit クラス宣言の中にあるウィジェットのメンバ変数をこのように宣言します:
 
  QMultiLineEdit *view;
エディタウィジェットの宣言
私たちが今しなければならないことはEditコンストラクタにウィジェットを作り、それを管理するためKTLWに教えることです。
 
  view= new QMultiLineEdit(this, "Main View");
  setView(view);
エディタウィジェットの作成

私たちは既にどうやってメニューアイテムを追加するかを知っています、よってloadとsaveのために新しいものを追加するのは簡単です。そのコードは'Exit' アイテムとほぼ同じです(今私たちには ID_SAVEのような定数が新しいコマンドのために必要なことに注意して下さい)。
 
  file->insertItem(klocale->translate("Open..."), ID_OPEN );
  file->insertItem(klocale->translate("Save"), ID_SAVE);
  file->insertItem(klocale->translate("Save As..."), ID_SAVEAS);
更にメニューアイテムの追加
省略('...')で終る全てのエントリーに注目して下さい。これはこのアイテムを選択することによってユーザーに更に情報を求めるダイアログが表示されるということを示しています。省略をこのように使うことはほぼ全ての世界のGUIの方法であり、あなたもこの慣習に是非従うべきです。

今私たちはメニューアイテムを作成しました。残りの仕事はそれらが何かをするようにすることです。必要なコードは非常に直接的です。最初に`Open' コマンドを見てみましょう。私たちがしなければならないことはommandCallback() スロットのswitchのところに新しいcaseを作ることです。
 
  case ID_OPEN:
    name= QFileDialog::getOpenFileName();
    if (!name.isEmpty()) {
      load(name);
    }
    break;
Editでファイルを開く
新しいはコードは現代的なファイルダイアログをポップアップし、ファイル名を得るためにQFileDialogクラスのスタティックメソッドを利用しています。もしユーザーが`Cancel'ボタンを押したら空の文字列が返されます。よって私たちはこれをテストしても何も起こりません。もしユーザーがファイル名を選択したら、loadメソッドを呼んでファイルを読み込みエディタウィジェットに渡します。

loadメソッドは選択したファイルの中身を読み込むためにQFileクラスを使います。QFileクラスはQtによって提供され、プラットフォーム非依存のファイルアクセスの方法を提供します。ここでは一回にファイルの中身を1行読むことのできるQTestStreamと組み合わせて使われています。それぞれの行は読まれるたびにテキストウィジェットに付加します。現在のファイル名のトラック名を残しておくことは重要なので(それによって'Save'がインプリメントできる)、即席の filename_ 変数をEditクラスに加えます。この値はloadメソッドによってセットされます。
 
  void Edit::load(const char *filename) {
      QFile f( filename );
      if ( !f.open( IO_ReadOnly ) )
          return;

      view->setAutoUpdate( FALSE );
      view->clear();

      QTextStream t(&f);
      while ( !t.eof() ) {
        QString s = t.readLine();
        view->append( s );
      }
      f.close();

      view->setAutoUpdate( TRUE );
      view->repaint();
      filename_= filename;
  }
load() メソッドのインプリメント
注意すべきこのメソッドの特徴はちらつきやパフォーマンスを向上させるためのsetAutoUpdate()の使いかたです。これにはファイル全てが読み込まれるまでテキストウィジェットに再描画を無効にするという効果があります、さもなければウィジェットは各行ごとに再描画するでしょう。ウィジェットをこの状態にしておかないことは重要です。よってファイルからの読み込みが終ったら、わたしたちは再びsetAutoUpdate()を呼び出して更新を有効にします。私たちはまた正しく再描画されたかを確かめるためrepaint() を呼び出します。

'Save As'コマンドはほとんど'Open'コマンドと同じです。私たちは getSaveFileName()を使います、これはgetOpenFileName()と違って選んだファイル名が存在するファイル名であることを要求しません。saveコマンドは簡単です。もし私たちがファイル名を知っているのなら文章を保存し、そうでないならsaveAs() コマンドが呼び出します。
 
    case ID_SAVE:
      if (!filename_.isEmpty())
        saveAs(filename_);
      else {
        name= QFileDialog::getSaveFileName();
        if (!name.isEmpty())
          saveAs(name);
      }
      break;
    case ID_SAVEAS:
      name= QFileDialog::getSaveFileName();
      if (!name.isEmpty())
        saveAs(name);
      break;
Editでのファイルの保存
実際にファイルを保存するコードは簡単です。わたしたちは以前QFileを使いましたが、今回は出力のためにファイルを開きたいことを示すためにIO_WriteOnly フラグを付けてファイルを開きます。わたしたちは全体のテキストをQMultiLineEditのtext()メソッドを使って得られる1(または複数)文字列として書き出します。
 
  void Edit::saveAs(const char *filename) {
      QFile f( filename );
      if ( !f.open( IO_WriteOnly ) )
          return;

      QTextStream t(&f);
      t << view->text();
      f.close();

      setHint(filename);
      filename_= filename;
  }
saveAs() メソッドのインプリメント
このバージョンの例の最後の特徴は`About'ボックスです。これは通常`Help'メニューの`About...' エントリーを使ってアクセスされるダイアログです。QMessageBox::about()スタティックメソッドを呼び出してここではインプリメントされます。`About' ボックスは通常アプリケーションのバージョン、作者、ベンダーなどの情報を表示するのに使われます。

    case ID_ABOUT:
      QMessageBox::about(this, "About Edit",
                         "This is a simple text editor example program.");
      break;
saveAs()メソッドのインプリメント

ユーザーインターフェースの改良

わたしたちのアプレットは今では有用な仕事をこなせますが、ユーザーインターフェースが貧弱です。たくさんの落度があります、例えば変更を保存しないで終了しようとした時の警告、現在開いているファイルの名前の表示、キーボードショートカット、などです。このような欠点を改良しようと思います。

最初に取り組むことはアプレットにステータスバーを追加することです。わたしたちはそれを現在開いているファイルの名前を表示させるのに使います。KDEはステータスバーウィジェットKStatusBarを提供し、KTLWはその管理ができるのでわたしたちがしなければならないことはそれほど残されていません。KStatusBarはメニューウィジェットととても似たAPIを持っているので何が行なわれているかあなたは分かるでしょう。ステータスバーを表示したあとKTLWにそれを管理させるために報告して下さい。私たちはID_HINTTEXT定数を使ってステータスバーに表示させる文字列を変えることができます。これは setHint()スロットにインプリメントします。
 
  void Edit::initStatusBar()
  {
    statusbar= new KStatusBar(this);
    statusbar->insertItem("Welcome to Edit", ID_HINTTEXT);
    statusbar->show();
    setStatusBar(statusbar);
  }

  void Edit::setHint(const char *text)
  {
    statusbar->changeItem(text, ID_HINTTEXT);
  }

ステータスバーを追加

私たちは例えば読み込みや保存のようにファイル名を変えるようなメソッドをユーザーが呼び出した時、ステータスバーに文字列をセットします。

今度はツールバーを追加します。これはちょっと複雑です、というのは私たちがボタンアイコンの割り当てやツールチップのセットをする必要があるからです。私たちはツールバーアイコンの読み込み(とキャッシング)を扱うためにKIconLoaderクラスを利用します。 このクラスはKDE File System Standardによって指定されるディレクトリでアイコン(私たちはただ普通の、用途に合うアイコンを使います)を検索します。 私たちはKApplicationによって作られるアイコンローダーにアクセスするためkapp.h によって提供されるマクロを使います。
 
  void Edit::initToolBar()
  {
    KIconLoader *loader = kapp->getIconLoader();
    toolbar = new KToolBar(this);

    toolbar->insertButton(loader->loadIcon("filenew.xpm"),
                          ID_NEW, TRUE,
                          klocale->translate("Create a new file"));
    toolbar->insertButton(loader->loadIcon("fileopen.xpm"),
                          ID_OPEN, FALSE, 
                          klocale->translate("Open a file"));
    toolbar->insertButton(loader->loadIcon("filefloppy.xpm"),
                          ID_SAVE, FALSE, 
                          klocale->translate("Save the current file"));

    addToolBar(toolbar);
    toolbar->setBarPos(KToolBar::Top);
    toolbar->show();
    connect(toolbar, SIGNAL(clicked(int)), SLOT(commandCallback(int)));
  }

ツールバーを追加
各ツールバーアイテムはinsertButton()を呼び出して作られます。このメソッドはアイコン(KIconLoaderを使ってアクセス)、コマンドid、ボタンが有効になったことを指すフラグ、ツールチップの文章、をパラメターとしてとります。私たちは通常i18nサポートのためにklocale->translate()でツールチップをラップします。

私たちはツールバー"clicked"シグナルをメニューで使ったのと同じスロットに接続します。これは全く安全です。というのはコマンドそれぞれに同じidナンバーを使っているので、わたしたちは多くのシグナルを欲しいだけスロットに自由に接続することができます。

ユーザーインターフェースのデザイン時にユーザーが働きようがない操作を呼び出すことを防止するのは一般的に良い練習です。私たちはこれをQtでアプリケーションの状態に依存して、ウィジェットを有効・無効にすることで行ないます。例えばもしあなたが文章を変更していないのに、元のファイルに保存することはナンセンスです(ただし違うファイルに保存することは有用です)。全てのQt,KDEのウィジェットはウィジェットを`grey out'し無効にする setEnabled() メソッドを持っています。

KMenuBarとKToolbarは両方ともコマンドidによって特定されるエントリーの状態をセットするメソッドを提供することによってそれらのエントリーを有効・無効にするプロセスを簡単に作れます。わたしたちはこれら両方enableCommand()のインプリメントの中で呼び出します。
 
  void Edit::enableCommand(int id) //スロット
  {
    toolbar->setItemEnabled(id, true);
    menu->setItemEnabled(id, true);
  }

  void Edit::disableCommand(int id) // スロット
  {
    toolbar->setItemEnabled(id, false);
    menu->setItemEnabled(id, false);
  }
コマンドを有効・無効にする
私たちは前後を考慮したコマンドの有効・無効のインプリメントのために、今これらのメソッドを使います。保存すべき変更がない時`Save'コマンドを無効にするために文章が変更されるまでトラックを保つ必要があります。これはユーザーが文章を変更した時にQMultiLineEdit によって発行されるtextChanged()シグナルを使って行なわれます。このシグナルを使うことで文章が変更されているかを指し示すフラグを作ることができます。これはEditクラスで定義されているtextChanged()スロットによって行なわれます。このスロットはコマンドの状態も更新します。
 
void Edit::textChanged()
{
  modified= true;
  enableCommand(ID_SAVE);
  enableCommand(ID_SAVEAS);
}
textChanged() スロットの定義
load,saveメソッドはまた、フラグを最新に保ち、コマンドを有効・無効にするために変更されなければならないということに注意して下さい。あなたはサンプルプログラムで、この変更を見ることができます。

あなたがこのセクションのエディタに最後に加えることは、エディタに保存されていない変更がある時にユーザーが終了しようとした時に表示される確認ダイアログです。
 


ダイアログはexit()メソッドを使ってインプリメントされます。このメソッドは単に変更を指すためにわたしたちが作ったフラグをテストし、警告ダイアログを表示するためにもうひとつのQMessageBoxスタティックメソッドを使います。
 
  int Edit::exit()
  {
    int die= 0;
 
    if (!modified)
      die= 1;
    else
      if (QMessageBox::warning(this, klocale->translate("Unsaved Changes"),
                               "You have unsaved changes, you will loose "
                               "them if you exit now.",
                               "Exit", "Cancel",
                               0, 1, 1))
         die= 0;
      else
        die= 1;

    return die;
  }
exit()メソッドの定義

間もなくできます...


This document is Copyright Richard J. Moore 1997-1998 (rich@kde.org)
You are free to distribute the examples given here in any form what so ever, and under any license terms. You are however NOT free to do the same with the text of the tutorial itself. You are permitted to distribute the text of the tutorial in electronic formats as long as it is distributed in its entirity. You may not charge for this document, though you may recoup packaging costs. You may make paper copies for personal use only. If you want to distribute this under other terms then drop me a mail.
翻訳者追記
オリジナルと比べて誤訳、おかしい記述があったら菰田泰生: tklab@ma6.seikyou.ne.jpまで連絡して下さい。なおライセンスについては元の文書に従います。