QMLのCalendarエレメントで日付入力を簡単に!

この記事は、Qt Advent Calendar 2015 - Qiitaの22日目(12/22)分です。
別件の原稿に追われていたので、前半は無理だなーと思いつつ、後半に参加登録したら去年と同じ日でした。
最終日は無理だけど、早すぎても厳しい。無難な日取りだったようです。


/// 今回の流れ ///
・エレメントの紹介
・日付入力を簡単に使用と思うまで
・日付入力サポート付きエディットの作成


/// エレメントの紹介 ///
さて、本題に。
QMLには、Calendarエレメントがあります(詳細は下記のサイトを参照してください)。

公式リファレンス:Calendar QML Type

このCalendarエレメントは、このような感じで配置すると使用できます。
Calendarエレメントには、指定した日付を強調したり、ユーザーがクリックした日付を取得したりなどカレンダーっぽい動きが実装されています。
import QtQuick 2.2
import QtQuick.Controls 1.2

ApplicationWindow {
  visible: true
  width: 640
  height: 480
  title: "Calendar Example"

  Calendar {
  }
}

そして、デフォルト状態でこのような感じのデザインです。
qml-calendar-0-default.png

/// 「日付入力を簡単にしよう!」と思うまで ///
なんだか簡単にスケジューラーアプリとか作れそうな気分になります。ですが、そうは問屋が卸してくれません。僕が知らないだけかもしれませんが、特定の日付を強調したり、ユーザーがクリックした日付を知ることくらいしかできなさそうに見えます。
ちゃんと使えそうなカレンダー機能は、Qt 5.6から追加されるっぽいです(先日のQt勉強会#30で調べている方がいらっしゃいました、ネタかぶったーw)。

公式リファレンス:Qt.labs.calendar Module

では、これは何に使えるのだろう? と考えた結果、いろいろなアプリケーションやWebサイトなどで見かける日付入力をサポートするところで使えるのではと考えました。
なので、適当なサイズに縮小して見よう! ってことで実際に縮小してみます。
import QtQuick 2.2
import QtQuick.Controls 1.2

ApplicationWindow {
  visible: true
  width: 640
  height: 480
  title: "Calendar Example"

  Calendar {
    width: 150
    height: 170
  }
}
qml-calendar-1-small.png

適当なサイズを指定してみるものの、小さくはなりますが、正直このままでは使用できません。
タッチパネルでの入力を考えるなら大きいままでも良いのですが、マウス前提ならコンパクトな方が良い場合も有ると思います。
今回は概ね見た目をどうにかしよう、と言う話だったことが発覚します。(あれ?w)


/// 日付入力サポート付きエディットの作成 ///
日付入力のサポートの場合、ユーザーが選択した日付を取得できればOKなので機能的な問題はありません。
問題なのは見た目だけです。見た目の変更には、CalendarStyleエレメントを使用します。詳細は下記のサイトです。

公式リファレンス:CalendarStyle QML Type

なお、これからデザインの変更方法を紹介すると共に、デザインのサンプルをお見せすることになりますが、それがイケてるかの保証はできません。あくまでも方法をお伝えするのみで、実際に格好良くするのは利用する皆さんです。あしからず。

見た目を~と言いつつ、まずは日付を選んで表示する部分の動きを実装します。

import QtQuick 2.2
import QtQuick.Controls 1.2
import QtQuick.Controls.Styles 1.2
import QtQuick.Layouts 1.1

ApplicationWindow {
  visible: true
  width: 640; height: 480
  title: "Calendar Example"

  //日付の文字列を作る
  function makeDateString(d){
    return "%1/%2/%3".arg(d.getFullYear()).arg(d.getMonth()+1).arg(d.getDate())
  }
  //日付の入力エリア
  TextField {
    id: inputText
    x: 10; y: 10
    readOnly: true
    horizontalAlignment: Text.AlignHCenter
    Component.onCompleted: {
      //未入力状態のときのテキストに今日の日付を設定
      placeholderText = makeDateString(new Date())
    }
    MouseArea {
      anchors.fill: parent
      onClicked: inputCalendar.visible = !inputCalendar.visible
    }
  }
  //カレンダー
  Calendar {
    id: inputCalendar
    width: 150
    height: 170
    anchors.left: inputText.left
    anchors.top: inputText.bottom
    visible: false

    //選択した日付をテキストボックスに入力
    onClicked: {
      inputText.text = makeDateString(date)
      visible = false
    }
  }
}

そんなに難しいことはありませんね。
TextFieldエレメントをクリックするとCalendarエレメントの表示/非表示をトグルします。
表示されたCalendarエレメントの日付部分をクリックするとCalendar::onClickedシグナルハンドラが呼び出されます。このハンドラには「date」という引数でDateのオブジェクトを取得できますので、その値を使用してTextFieldエレメントの内容を変更します。

それでは、デザインの方を変更します。今回は使用しない設定項目(プロパティ)もコメント状態で入れていますので参考にしてください。
  Calendar {
// 中略

    style: CalendarStyle {
      //グリッドの色
      //      gridColor: "white"
      //グリッドの表示非表示
      gridVisible: false
      //ナビゲーションバー
      navigationBar: Rectangle {
        height: 20
        RowLayout {
          anchors.fill: parent
          anchors.margins: 1
//          //前の年へのボタン
//          Button {
//            text: "<<"
//            Layout.fillHeight: true
//            Layout.preferredWidth: 20
//            onClicked: inputCalendar.showPreviousYear()
//          }
          //前の月へのボタン
          Button {
            text: "<"
            Layout.fillHeight: true
            Layout.preferredWidth: 20
            onClicked: inputCalendar.showPreviousMonth()
          }
          //表示中の月の表示
          Text {
            Layout.fillWidth: true
            horizontalAlignment: Text.AlignHCenter
            text: styleData.title
          }
          //次の月へのボタン
          Button {
            text: ">"
            Layout.fillHeight: true
            Layout.preferredWidth: 20
            onClicked: inputCalendar.showNextMonth()
          }
//          //次の年へのボタン
//          Button {
//            text: ">>"
//            Layout.fillHeight: true
//            Layout.preferredWidth: 20
//            onClicked: inputCalendar.showNextYear()
//          }
        }
      }

      //曜日のマスの設定
      dayOfWeekDelegate: Text {
        height: 20
        verticalAlignment: Text.AlignVCenter
        horizontalAlignment: Text.AlignHCenter

        text: dayOfWeekString(styleData.dayOfWeek)
        function dayOfWeekString(id){
          var str;
          switch(id){
          case Locale.Sunday:   str="Su"; break;
          case Locale.Monday:   str="Mo"; break;
          case Locale.Tuesday:  str="Tu"; break;
          case Locale.Wednesday:str="We"; break;
          case Locale.Thursday: str="Th"; break;
          case Locale.Friday:   str="Fr"; break;
          case Locale.Saturday: str="Sa"; break;
          default:              str="";   break;
          }
          return str
        }
      }

      //日付のマスの設定
      dayDelegate: Rectangle {
        //背景色のグラデーション(標準は無色)
        gradient: Gradient {
          GradientStop { position: 0; color: "#ffffff" }
          GradientStop { id: dayGradi; position: 1; color: "#eeeeee" }
        }
        //マスの状態で表示を変更する
        states: [ State {
            when: styleData.selected  //選択箇所のとき
            PropertyChanges { target: dayGradi; color: "#66ff0000" }
          }
          ,State {
            when: styleData.hovered   //マウスオーバーのとき
            PropertyChanges { target: dayGradi; color: "#66ffaa00" }
          }
          ,State {
            when: styleData.today     //今日のとき
            PropertyChanges { target: dayGradi; color: "#220000ff" }
          }
        ]
        //日付の数字
        Label {
          text: styleData.date.getDate()
          anchors.centerIn: parent
          color: styleData.visibleMonth ? "black" : "lightGray" //表示中の月のときハッキリ
          font.pointSize: 8
        }
        //マスの枠線(右)
        Rectangle {
          width: 1
          anchors.right: parent.right
          anchors.top: parent.top
          anchors.bottom: parent.bottom
          anchors.topMargin: 2
          opacity: 0.5
          color: "#000066"
        }
        //マスの枠線(下)
        Rectangle {
          height: 1
          anchors.left: parent.left
          anchors.right: parent.right
          anchors.bottom: parent.bottom
          anchors.leftMargin: 2
          opacity: 0.5
          color: "#000066"
        }
      }
    }
  }

CalendarStyle::navigationBarプロパティで、カレンダーの上にくっついているボタンやら何やらの塊のデザインを変更します。
ここは、見た目を変更するというよりは、完全に作り直すイメージになります。なので、今回は、
 ・前の年へ移動するボタン(非表示)
 ・前の月へ移動するボタン
 ・表示している月の文字列
 ・次の月へ移動するボタン
 ・次の年へ移動するボタン(非表示)
を、配置します。
なお、各ボタンが押されたときの動作として、表示している月を変更する関数を呼び出します。それぞれ以下の通りです。
 ・前の年へ CalendarStyle::showPreviousYear()
 ・前の月へ CalendarStyle::showPreviousMonth()
 ・前の月へ CalendarStyle::showNextMonth()
 ・前の年へ CalendarStyle::showNextYear()

CalendarStyle::dayOfWeekDelegateプロパティで、曜日の部分のデザインを変更します。
このプロパティは、名前にDelegateがついているところからも分かるとおり、1つ分のデザインを作成します。
それぞれの曜日で使用する共通デザインなので、以下のプロパティで実行時の曜日を知ることができます。これを使用して表示する文字列を生成します。今回は縮小する方向なのでアルファベットで省スペースな感じにします。
 ・曜日:styleData.dayOfWeek

CalendarStyle::dayDelegateプロパティで、日付の部分のデザインを変更します。
このプロパティも、曜日と同様で1つ分のデザインを作成します。
デザインの内容は省きますが、リアルな当日や選択箇所を強調するために以下のプロパティを使用しています。
それぞれ該当するときtrueになります。
 ・選択箇所のとき:styleData.selected
 ・マウスオーバーのとき:styleData.hovered
 ・今日のとき:styleData.today
日付情報は、以下のプロパティで取得できます。これはDate()オブジェクトです。なので、サンプルコードでは、「styleData.date.getDate()」としています。
 ・styleData.date
また、日付部分は、表示している前後の月も描画対象になるので、以下のプロパティで判定します。カレンダーは長方形なので前月の最後と翌月の最初はグレーにします。
 ・styleData.visibleMonth

ざっと、こんな感じです。
これで、皆さんも格好良いor可愛いカレンダーがデザインできるはずです。やってみましょう!
入力補助程度であれば十分に使えると思います。(タブンネ-)