画面作成のためのKotlin入門~Kotlinの基本を学習する~

イントロダクション

Kotlinで画面の作製、GUIを作り、ゲームを作成しようと思っています。
KorGEというゲームエンジンをインストールして、サンプルプロジェクトを動かしたところで「基本がわからん!」となり、学習することにしました。

学習したいポイントは以下の通りです。

  1. 基本文法
  2. ライブラリなどのインポート方法
  3. GUIの作り方

まだまだ、基本の学習中でコードの解析が終わらないので引き続きこの記事は更新いたします。

問題1

Kotlinは開発環境にすごーく依存するため、ライブラリなどGradleでの設定が必要になるので。。。

解決策

サンプルプロジェクトをカスタムする方向でプログラムを学習&作成していくことにしました。

参考サイト

  1. korlibs.kogeパッケージのAPIドキュメント
  2. KorGEのサイト
  3. KorGEのドキュメントサイト

今回は、GUIアプリを作成したいので、サンプルに「2048」というゲームをカスタムします。
2048ゲームの画面

Kotlinの基本

まずは、サンプルコードを見てみます。結構長いです。。。
そのため、上記のリンク先にコードがあります。

基本を学習するにあたり、サンプルコードを見ながら「ここは、こーなっている。。。」という形で学習していきます。

import文について

サンプルコードの下のようなコードがこれに当たります。

import korlibs.event.*
import korlibs.korge.*
import korlibs.korge.animate.*
 ・
 ・
 ・

各ライブラリのクラスをインポートしています。Javaと同じです。
そして、自分で作成したクラスなどもインポートできます。

ちなみにJava言語での「static import」が使えるかについては「NO」通常のimport文でインポートできるようです。使えるようにできるということです。
stackoverfllow参照

変数の扱い

下のように定義できます。

// 定数
val x: Int = 5
// 変更可能な普通の変数
var y: Int = 5
// インクリメント
y += 1

省略した書き方

val x = 5

関数と変数の定義

val PI = 3.14
var x = 0

fun incrementX() {
    x += 1
}

varとvalの違い

サンプルコードには下のようなコードがあります。「var」と「val」、そして「fun」で始まる行があります。
結論から言うとJava言語でいうなら

  • 「val」は「final」修飾子つきの変数です。
    つまり定数です。クラスの場合は、インスタンスが変更されなければエラーになりません。
  • 「var」は通常の変数です。
    つまり、値の書き換えなど変更ができます。
  • 「fun」は関数を示す。
    fun numberFor(blockId: Int) = blocks[blockId]!!.number

    上の関数は、blocks配列のblockId番目のnumberを返します。

ちなみに、サンプルコードの一部を抜粋、読んで理解しているところです。

var cellSize: Float = 0f
var fieldSize: Float = 0f
var leftIndent: Float = 0f
var topIndent: Float = 0f
var font: BitmapFont by Delegates.notNull()

fun columnX(number: Int) = leftIndent + 10 + (cellSize + 10) * number
fun rowY(number: Int) = topIndent + 10 + (cellSize + 10) * number

var map = PositionMap()
val blocks = mutableMapOf<Int, Block>()
var history: History by Delegates.notNull()

fun numberFor(blockId: Int) = blocks[blockId]!!.number
fun deleteBlock(blockId: Int) = blocks.remove(blockId)!!.removeFromParent()

val score = ObservableProperty(0)
val best = ObservableProperty(0)

var freeId = 0
var isAnimationRunning = false
var isGameOver = false

文字列の扱い

変数と文字列を同時に使用する場合、Java言語で書くと右のような書き方「println("XXX" + a)」の処理を行いたい場合

var a = 1
// simple name in template:
val s1 = "a is $a" 

a = 2
// arbitrary expression in template:
val s2 = "${s1.replace("is", "was")}, but now is $a"

「$」を使用するようです。どこかのフレームワークで見たような。。。感じです。
注意点として、Stringクラスのメソッドを使用したい時などは「{}」で囲う必要があるみたいです。

クラスの作り方

コンストラクタで作成する感じ?

class Rectangle(val height: Double, val length: Double) {
    val perimeter = (height + length) * 2 
}
fun main() {
    val rectangle = Rectangle(5.0, 2.0)
    println("The perimeter is ${rectangle.perimeter}")
}

継承関係の作り方「:」を使用する。Shapeクラスを継承したRectangleクラス

open class Shape

class Rectangle(val height: Double, val length: Double): Shape() {
    val perimeter = (height + length) * 2
}

メインメソッド

引き続き読み進めていきます。次はメインメソッドを読んでいきます。ちょっと長いので一部抜粋しながら読み進めます。

まずは、下のコードです。メインメソッドの書き始め部分です。通常のメインメソッドは下のように書くのですが。。。

fun main(args:Array<String>){
    println("Hello Kotlin")
}

もしくは、下のように書きます。

fun main() {
    println("Hello world!")
}

標準入出力

標準入力は、ユーザーの入力、出力はコンソールなどに出力することを言います。
Kotlinの場合は簡単に記述できます。

println("入力してください: ")

// ユーザーの入力を受け付けます
val yourWord = readln()

print("標準出力に入力した文字を出力: ")
print(yourWord)

関数(Function)の定義方法

Kotlinで定義する場合の書き方です。それぞれ引数あり、なし、返り値あり、なしの書き方を記載します。

引数と返り値ありの関数

引数あり、返り値ありの関数。引数も返り値もInt型です。

fun sum(a: Int, b: Int): Int {
    return a + b
}

簡単な課書き方をすると下のようになります。

fun sum(a: Int, b: Int) = a + b

引数あり、返り値なしの関数

引数あり、返り値なしの場合です。

fun printSum(a: Int, b: Int): Unit {
    println("sum of $a and $b is ${a + b}")
}

引数なし、返り値なしの関数

fun printSum() {
    println("sum of $a and $b is " + (1 + 2))
}

KorGEの場合

KorGEでは違うようです。これもひとつずつ分解指定解析します。1回解析してしまえばあとは、見ただけでわかります(笑)

初めの疑問点

メインメソッドの「suspend fun main()」の「suspend」の部分です。どんな意味があるのでしょうか?

suspend fun main() = Korge(
    virtualSize = Size(480, 640),
    title = "2048",
    bgcolor = RGBA(253, 247, 240),
    /**
        `gameId` is associated with the location of storage, which contains `history` and `best`.
        see [Views.realSettingsFolder]
     */
    gameId = "io.github.rezmike.game2048",
    forceRenderEveryFrame = false, // Optimization to reduce battery usage!
) { ... }

ここでの注意点は「suspend」キーワードです。このキーワードはこちらのページを参考に学習しました。またこちらのページでは「Coroutineとsuspendの違い」について学習しました。

結論を書くと下のようになります。ChatGPT先生に聞いたら簡潔に答えてくれました。

  • Coroutineは非同期処理全体を管理するためのメカニズム
  • suspend関数(修飾子?)はCoroutine内で一時停止可能な関数であり、非同期処理の具体的な一部を担います。

つまりsuspendは

「非同期処理とか、一時停止可能な関数(function)ですよ」という意味でした。
具体的には、現在のアプリが動いているデバイス以外へリクエストなどを送信しても、結果が返ってくるのを待たずに処理が動くとか
レスポンスを待って処理を行うとかが可能という意味でス。

「どうやって?」という部分に関しては今後調査していく必要があります。しかし、現段階ではここでSTOPしておきます。

次の疑問点

「main() = XXX」の部分です。具体的には、下の部分です。

fun main() = Korge(
    virtualSize = Size(480, 640),
    title = "2048",
    bgcolor = RGBA(253, 247, 240),
    /**
        `gameId` is associated with the location of storage, which contains `history` and `best`.
        see [Views.realSettingsFolder]
     */
    gameId = "io.github.rezmike.game2048",
    forceRenderEveryFrame = false, // Optimization to reduce battery usage!
)

こちらのサイトを参考にすると「インライン関数」として書いてあるようですが、ちょっと違うような気がします。

次いで調べたのがKotlinのドキュメントページです、英語ですが、日本語に変換して読みます。
Kotlin-Flllowを使用してデバックする」という記事にありました。
コードとしては、下のようなコードで、「メソッドの内容を書き換える」書き方です。。。

fun main() = runBlocking {
    simple()
        .collect { value ->
            delay(300)
            println(value)
        }
}

上記のコードは、下のコードをデバックするためのコードのようです。
つまり、メインメソッドにブロックを使用しrunBlocking()メソッドでコルーチンをラップします。
事前に定義されているのが「runBlocking()」でコルーチンの仕組みの一つです。
このメソッドをオーバーライドしてデバックを行っているというわけですね。なるほど!

import kotlinx.coroutines.*
import kotlinx.coroutines.flow.*
import kotlin.system.*

fun simple(): Flow<Int> = flow {
    for (i in 1..3) {
        delay(100)
        emit(i)
    }
}

結局は。。。

問題の「main() = KorGE( ... )の部分はKorGEフレームワークでメインメソッドを書き換えているということで、納得しました。

つまりは、上のrunBlocking()関数で書き換えたときと同じように、KorGEクラスで書き換えた、オーバーライドしてメインの処理を実行するようにした
ということです。
そのため、下のようにコンストラクタ部分に、画面の情報をセットしています。

Korge(
    virtualSize = Size(480, 640),
    title = "2048",
    bgcolor = RGBA(253, 247, 240),
    ・
    ・
    ・

そして、処理(main)部分がそのあとに続きます。

suspend main() = KorgGE(
    ... // 画面サイズなどのプロパティを設定
) {
    メインの処理。。。
}

サンプルコード

Kotlinの基本に戻る

import korlibs.event.*
import korlibs.korge.*
import korlibs.korge.animate.*
import korlibs.korge.input.*
import korlibs.korge.service.storage.*
import korlibs.korge.tween.*
import korlibs.korge.ui.*
import korlibs.korge.view.*
import korlibs.image.color.*
import korlibs.image.font.*
import korlibs.image.format.*
import korlibs.image.text.TextAlignment
import korlibs.io.async.*
import korlibs.io.async.ObservableProperty
import korlibs.io.file.std.*
import korlibs.korge.style.styles
import korlibs.korge.style.textColor
import korlibs.korge.style.textFont
import korlibs.korge.style.textSize
import korlibs.korge.view.align.*
import korlibs.math.geom.*
import korlibs.math.interpolation.*
import korlibs.time.seconds
import kotlin.collections.set
import kotlin.properties.*
import kotlin.random.*

var cellSize: Float = 0f
var fieldSize: Float = 0f
var leftIndent: Float = 0f
var topIndent: Float = 0f
var font: BitmapFont by Delegates.notNull()

fun columnX(number: Int) = leftIndent + 10 + (cellSize + 10) * number
fun rowY(number: Int) = topIndent + 10 + (cellSize + 10) * number

var map = PositionMap()
val blocks = mutableMapOf<Int, Block>()
var history: History by Delegates.notNull()

fun numberFor(blockId: Int) = blocks[blockId]!!.number
fun deleteBlock(blockId: Int) = blocks.remove(blockId)!!.removeFromParent()

val score = ObservableProperty(0)
val best = ObservableProperty(0)

var freeId = 0
var isAnimationRunning = false
var isGameOver = false

suspend fun main() = Korge(
    virtualSize = Size(480, 640),
    title = "2048",
    bgcolor = RGBA(253, 247, 240),
    /**
        `gameId` is associated with the location of storage, which contains `history` and `best`.
        see [Views.realSettingsFolder]
     */
    gameId = "io.github.rezmike.game2048",
    forceRenderEveryFrame = false, // Optimization to reduce battery usage!
) {
    font = resourcesVfs["clear_sans.fnt"].readBitmapFont()

    val storage = views.storage
    history = History(storage.getOrNull("history")) {
        storage["history"] = it.toString()
    }
    best.update(storage.getOrNull("best")?.toInt() ?: 0)

    score.observe {
        if (it > best.value) best.update(it)
    }
    best.observe {
        storage["best"] = it.toString()
    }

    cellSize = views.virtualWidth / 5f
    fieldSize = 50 + 4 * cellSize
    leftIndent = (views.virtualWidth - fieldSize) / 2
    topIndent = 150f

    val bgField = roundRect(Size(fieldSize, fieldSize), RectCorners(5), fill = Colors["#b9aea0"]) {
        position(leftIndent, topIndent)
    }
    graphics {
        fill(Colors["#cec0b2"]) {
            for (i in 0..3) {
                for (j in 0..3) {
                    roundRect(
                        10 + (10 + cellSize) * i, 10 + (10 + cellSize) * j,
                        cellSize, cellSize,
                        5f
                    )
                }
            }
        }
    }.position(leftIndent, topIndent)

    val bgLogo = roundRect(Size(cellSize, cellSize), RectCorners(5), fill = RGBA(237, 196, 3)) {
        position(leftIndent, 30f)
    }
    text("2048", cellSize * 0.5f, Colors.WHITE, font).centerOn(bgLogo)

    val bgBest = roundRect(Size(cellSize * 1.5, cellSize * 0.8), RectCorners(5f), fill = Colors["#bbae9e"]) {
        alignRightToRightOf(bgField)
        alignTopToTopOf(bgLogo)
    }
    text("BEST", cellSize * 0.25f, RGBA(239, 226, 210), font) {
        centerXOn(bgBest)
        alignTopToTopOf(bgBest, 5.0)
    }
    text(best.value.toString(), cellSize * 0.5f, Colors.WHITE, font) {
        setTextBounds(Rectangle(0f, 0f, bgBest.width, cellSize - 24f))
        alignment = TextAlignment.MIDDLE_CENTER
        alignTopToTopOf(bgBest, 12.0)
        centerXOn(bgBest)
        best.observe {
            text = it.toString()
        }
    }

    val bgScore = roundRect(Size(cellSize * 1.5f, cellSize * 0.8f), RectCorners(5.0f), fill = Colors["#bbae9e"]) {
        alignRightToLeftOf(bgBest, 24.0)
        alignTopToTopOf(bgBest)
    }
    text("SCORE", cellSize * 0.25f, RGBA(239, 226, 210), font) {
        centerXOn(bgScore)
        alignTopToTopOf(bgScore, 5.0)
    }
    text(score.value.toString(), cellSize * 0.5f, Colors.WHITE, font) {
        setTextBounds(Rectangle(0f, 0f, bgScore.width, cellSize - 24f))
        alignment = TextAlignment.MIDDLE_CENTER
        centerXOn(bgScore)
        alignTopToTopOf(bgScore, 12.0)
        score.observe {
            text = it.toString()
        }
    }

    val btnSize = cellSize * 0.3
    val restartImg = resourcesVfs["restart.png"].readBitmap()
    val undoImg = resourcesVfs["undo.png"].readBitmap()
    val restartBlock = container {
        val background = roundRect(Size(btnSize, btnSize), RectCorners(5f), fill = RGBA(185, 174, 160))
        image(restartImg) {
            size(btnSize * 0.8, btnSize * 0.8)
            centerOn(background)
        }
        alignTopToBottomOf(bgBest, 5.0)
        alignRightToRightOf(bgField)
        onClick {
            this@Korge.restart()
        }
    }
    val undoBlock = container {
        val background = roundRect(Size(btnSize, btnSize), RectCorners(5f), fill = RGBA(185, 174, 160))
        image(undoImg) {
            size(btnSize * 0.6, btnSize * 0.6)
            centerOn(background)
        }
        alignTopToTopOf(restartBlock)
        alignRightToLeftOf(restartBlock, 5.0)
        onClick {
            this@Korge.restoreField(history.undo())
        }
    }

    if (!history.isEmpty()) {
        restoreField(history.currentElement)
    } else {
        generateBlockAndSave()
    }

    root.keys.down {
        when (it.key) {
            Key.LEFT -> moveBlocksTo(Direction.LEFT)
            Key.RIGHT -> moveBlocksTo(Direction.RIGHT)
            Key.UP -> moveBlocksTo(Direction.TOP)
            Key.DOWN -> moveBlocksTo(Direction.BOTTOM)
            else -> Unit
        }
    }

    onSwipe(20.0) {
        when (it.direction) {
            SwipeDirection.LEFT -> moveBlocksTo(Direction.LEFT)
            SwipeDirection.RIGHT -> moveBlocksTo(Direction.RIGHT)
            SwipeDirection.TOP -> moveBlocksTo(Direction.TOP)
            SwipeDirection.BOTTOM -> moveBlocksTo(Direction.BOTTOM)
        }
    }
}

Dockerの使い方

イントロダクション

ラズパイにRedmineをインストールして使用しようと考えております。
しかし、記事を検索しながら、いろいろとやってみると、どれもこれもエラーがでて使えない。。。結局Dockerをちゃんと学習することにしました。

参考サイト

DockerDocsを参考にします。

タイトルは「Raspberry Pi OS (32 ビット) に Docker エンジンをインストールする」です。

Dockerとは?

いろいろと記事を斜め読みしてみると下のようなものということです。

Dockerはコンテナ仮想化を用いたOSレベルの仮想化によりアプリケーションを開発・実行環境から隔離し、アプリケーションの素早い提供を可能にする。

色々な説明があるけど、結局はこういうことだと思う。

つまり、「コンテナ」にまとめたアプリケーションを仮想マシンを使用して実行できる。ということらしい

Docker事始め

次のコマンドで、不要なもの、古いDockerをアンインストールします。もしかしたらインストールしているかもしれないからです。

よくあるのが、デフォルトインストールしているかもしれません。。。

for pkg in docker.io docker-doc docker-compose podman-docker containerd runc; do sudo apt-get remove $pkg; done

DockerのAPTリポジトリセットアップ

コードを書くと下のようになります。

# Add Docker's official GPG key:
sudo apt-get update
sudo apt-get install ca-certificates curl gnupg
sudo install -m 0755 -d /etc/apt/keyrings
curl -fsSL https://download.docker.com/linux/raspbian/gpg | sudo gpg --dearmor -o /etc/apt/keyrings/docker.gpg
sudo chmod a+r /etc/apt/keyrings/docker.gpg

# Set up Docker's APT repository:
echo \
  "deb [arch=$(dpkg --print-architecture) signed-by=/etc/apt/keyrings/docker.gpg] https://download.docker.com/linux/raspbian \
  $(. /etc/os-release && echo "$VERSION_CODENAME") stable" | \
  sudo tee /etc/apt/sources.list.d/docker.list > /dev/null
sudo apt-get update

ラズパイを更新

下のコマンドで、ラズパイの更新します。

sudo apt-get update

コマンドのインストール

下のコマンドをインストールします。既にインストールしているときは「すでに最新バージョン」というメッセージを確認できます。

  • ca-certificates
  • gnupg
  • curl
sudo apt-get install ca-certificates curl gnupg

Dockeraptリポジトリをセットアップ準備

GPGコマンドで暗号化・復号を行えるようにする?

sudo install -m 0755 -d /etc/apt/keyrings
curl -fsSL https://download.docker.com/linux/raspbian/gpg | sudo gpg --dearmor -o /etc/apt/keyrings/docker.gpg
sudo chmod a+r /etc/apt/keyrings/docker.gpg

Dockeraptリポジトリをセットアップ

echo \
  "deb [arch=$(dpkg --print-architecture) signed-by=/etc/apt/keyrings/docker.gpg] https://download.docker.com/linux/raspbian \
  $(. /etc/os-release && echo "$VERSION_CODENAME") stable" | \
  sudo tee /etc/apt/sources.list.d/docker.list > /dev/null

最後に更新

sudo apt-get update

Dockerパッケージインストール

sudo apt-get install docker-ce docker-ce-cli containerd.io docker-buildx-plugin docker-compose-plugin

インストールできたら、起動確認「ハローワールド」を実行する

sudo service docker start
sudo docker run hello-world

Dockerコマンドを使う

Dockerをコマンドで扱うときの基本構文は下のようになっています。※こちらのページ参照

docker run [OPTIONS] IMAGE[:TAG|@DIGEST] [COMMAND] [ARG...]

DockerFileを使う

こちらのページを参照しています。※英語だったので翻訳してみました。
下のようなコマンドがあるのでそれを使用するということらしい。。。
Dockerのコマンド入力の基本としてコマンド名は大文字、引数は小文字で書くようです。

docker ADD argument...
コマンド名 意味
ADD ホスト上のソースから、設定された宛先にあるコンテナー独自のファイルシステムにファイルをコピーします。
CMD コンテナー内で特定のコマンドを実行するために使用できます。
ENTRYPOINT は、イメージを使用してコンテナーが作成されるたびに使用されるデフォルトのアプリケーションを設定します。
ENV 環境変数を設定します。
EXPOSE 特定のポートを関連付けて、コンテナと外部の間のネットワークを可能にします。
FROM ビルド プロセスの開始に使用されるベース イメージを定義します。
MAINTAINER イメージ作成者のフルネームと電子メール アドレスを定義します。
RUN Dockerfile の中心的な実行ディレクティブです。
USER コンテナを実行する UID (またはユーザー名) を設定します。
VOLUME コンテナからホスト マシン上のディレクトリへのアクセスを可能にするために使用されます。
WORKDIR CMD で定義されたコマンドが実行されるパスを設定します。
LABEL Docker イメージにラベルを追加できます。

試しにコマンドを実行する

下のようにディレクトリを作成します。作成先は初期ディレクトリ(ユーザーのディレクトリ)から「dockerbuild」というディレクトリを作成しました。

mkdir ~/dockerbuild

Dockerfileを作成します。

nano Dockerfile

下の内容をコピペします。

FROM ubuntu:latest
MAINTAINER NAME EMAIL

RUN apt-get -y update
RUN apt-get -y upgrade
RUN apt-get install -y build-essential

Where NAME is your full name and EMAIL is your email address.

そして、以下のようにコマンドをたたきます。

docker build -t "NAME:Dockerfile" .


エラーがあるけど、イメージ作製はできているようです。

「NAME」には好きな名前を入れてよいです。最後の「.」を忘れないようにしましょう。

これができたら下のようなコマンドをたたきます。

docker images

使用できるイメージの一覧が表示されます。

Redmineのインストール

こちらのサイトを参考にします。
手順は以下の通りです。

  1. docker-compose.ymlを作成する
  2. docker-composeコマンドを実行する
    docker-compose up -d

しかしうまくいかない。。。

no matching manifest for linux/arm/v7 in the manifest list entries

DBの指定とYMLの作成

結局docker-compose.ymlの記述がまずかったみたいだ。「hypriot/rpi-mysql」を指定してやれば問題なかった。
つまり、下の様に書くとOK
<docker-compose.yml>

version: '3.8'
services:
  redmine:
    image: redmine
    container_name: redmine
    ports:
      - 3000:3000
    volumes:
      - ./data/plugins:/usr/src/redmine/plugins
      - ./data/themes:/usr/src/redmine/public/themes
    environment:
      REDMINE_DB_MYSQL: redmine-db
      REDMINE_DB_PASSWORD: redmine
    depends_on:
      - redmine-db
    restart: always

  redmine-db:
    image: hypriot/rpi-mysql
    container_name: redmine-db
    ports:
      - 3306:3306
    environment:
      MYSQL_ROOT_PASSWORD: redmine
      MYSQL_DATABASE: redmine
    volumes:
      - ./data/db:/var/lib/mysql
    command: mysqld --character-set-server=utf8 --collation-server=utf8_unicode>
    restart: always

しかしポート番号が。。。

for redmine-db  Cannot start service redmine-db: driver failed programming external connectivity on endpoint redmine-db (af1d40![](http://zenryokuservice.com/wp/wp-content/uploads/2023/11/Docker4.png)9b7e96abcb6b0d803518c400dd6f2bca79ef35fced14fd6a20b3c2b9cf): Error starting userland proxy: listen tcp4 0.0.0.0:3306: bind: address already in use

つまりは、「DBが使用するポート番号が使用されているため、DBが動かせない」ということでした。

使用ポートを調べる

「ss」コマンドで調べます。実行すると下のような結果が出ました。

pi@raspberrypi:~/dockerbuild/redmine $ ss -atn
State   Recv-Q  Send-Q   Local Address:Port        Peer Address:Port   Process  
LISTEN  0       5              0.0.0.0:5900             0.0.0.0:*               
LISTEN  0       128          127.0.0.1:631              0.0.0.0:*               
LISTEN  0       128            0.0.0.0:22               0.0.0.0:*               
LISTEN  0       80           127.0.0.1:3306             0.0.0.0:*               
ESTAB   0       0            127.0.0.1:60324          127.0.0.1:34449           
ESTAB   0       0            127.0.0.1:34449          127.0.0.1:60324           
ESTAB   0       0        169.254.37.79:5900     169.254.221.230:55375           
LISTEN  0       5                 [::]:5900                [::]:*               
LISTEN  0       511                  *:80                     *:*               
LISTEN  0       128               [::]:22                  [::]:*               
LISTEN  0       128              [::1]:631                 [::]:*  

まずは、使用しているポートが80番で待機(LISTENING)しているサービスと言えば「apache2」があやしい。。。ウェブサーバーは大体80を使っていることが多いので。。。下のコマンドでサービスを確認

service --status-all


そんなわけで、アパッチ(apache2)を停止し用と思ったけど、ポートを変更して対応します。

YMLファイルの編集

ポート番号を3306から3307に変更しました。

  redmine-db:
    image: hypriot/rpi-mysql
    container_name: redmine-db
    ports:
      - 3307:3307
    environment:
      MYSQL_ROOT_PASSWORD: redmine
      MYSQL_DATABASE: redmine
    volumes:
      - ./data/db:/var/lib/mysql
    command: mysqld --character-set-server=utf8 --collation-server=utf8_unicode>
    restart: always

起動することができました。

しかし、現状では不要なアプリケーションが沢山動いているようなので、ブラウザで開けませんでした。。。
他のサービスなどを停止すればよいのですが、また後日。。。

ラズパイの準備

Redmineが動いたので、今度はラズパイの運用準備を行います。
大まかに行うことは以下の通りです。

  • 使用しないGPUのメモリ割り当てを減らす
  • 使わないアプリの停止

メモリ使用量確認

  • free -hコマンド:メモリの利用状況を確認
  • df -h: ファイル・システム内のフリー・スペースの量を表示する

デーモンを停止する

デーモンとは、画面上に見えないアプリケーションのことです。停止するのは以下のデーモンです。

  • alsa-utils
  • dbus
  • triggerhappy
  • cups
プロセルの確認方法
ps aux

上記のコマンドで確認できます。

サービス一覧を取得する

下のコマンドで一覧できます。

service --status-all

ここで表示されたサービスの名前を停止するのには下のように行います。

[ - ]  alsa-utils
 [ - ]  apache-htcacheclean
 [ + ]  apache2
 [ - ]  apparmor
 [ + ]  avahi-daemon
 [ + ]  binfmt-support
 [ + ]  bluetooth
 [ - ]  cgroupfs-mount
 [ - ]  console-setup.sh
 [ + ]  cron
 [ + ]  cups
 [ + ]  cups-browsed
 [ + ]  dbus
 [ + ]  docker
 [ + ]  dphys-swapfile
 [ + ]  fake-hwclock
 [ - ]  fio
 [ - ]  hwclock.sh
 [ - ]  keyboard-setup.sh
 [ + ]  kmod
 [ + ]  lightdm
 [ + ]  mariadb
 [ + ]  networking
 [ - ]  nfs-common
 [ - ]  paxctld
 [ - ]  plymouth
 [ + ]  plymouth-log
 [ + ]  procps
 [ - ]  pulseaudio-enable-autospawn
 [ + ]  raspi-config
 [ + ]  rng-tools-debian
 [ - ]  rpcbind
 [ - ]  rsync
 [ + ]  rsyslog
 [ - ]  saned
 [ + ]  ssh
 [ - ]  sudo
 [ - ]  triggerhappy
 [ + ]  udev
 [ - ]  x11-common

サービスの停止方法

下記コマンドでサービスの停止

sudo systemctl サービス名

無効にする

sudo systemctl disable サービス名

実行したコマンド

<サービスの停止>

sudo systemctl stop alsa-utirls
sudo systemctl stop dbus
sudo systemctl stop triggerhappy.service triggerhappy.service
sudo systemctl stop triggerhappy.socket
sudo systemctl stop cups

<サービス自動起動無効>

sudo systemctl disable alsa-utirls
sudo systemctl disable dbus
sudo systemctl disable triggerhappy.service triggerhappy.service
sudo systemctl disable triggerhappy.socket
sudo systemctl disable cups

設定の反映を行うのに再起動します。

sudo reboot

結果

<サービス停止前>

               total        used        free      shared  buff/cache   available
Mem:           921Mi       328Mi        55Mi        10Mi       538Mi       526Mi
Swap:          384Mi        68Mi       316Mi

<サービス停止後>

               total        used        free      shared  buff/cache   available
Mem:           921Mi       376Mi        78Mi       8.0Mi       466Mi       479Mi
Swap:          376Mi       1.0Mi       375Mi

起動スクリプトを作る

まずは、Shellスクリプトである宣言をする

# !/bin/sh

処理を作成する(自分の場合です。)

cd ~/dockerbuild/redmine/

起動する設定を行う

参考サイトはこちらです。
/etc/systemd/system/ ディレクトリに autorun.serviceを、以下のように作成する

[Unit]
Description=Execute at OS startup and terminates
After=network.target
[Service]
Type=oneshot
ExecStart=/home/pi/autorun.sh
ExecStop=/home/pi/os_term.sh
RemainAfterExit=true
[Install]
WantedBy=multi-user.target

After=network.target はネットワーク関連のプロセスが起動した後に当サービスを起動するという意味らしいです。
サービスの有効化

sudo systemctl daemon-reload
sudo systemctl enable autorun.service

Redmineの起動確認

Redmineの使い方

まずは、初期設定を行う。

チケットのトラッカー

チケットのトラッカーを追加してやります。
上部のメニューにある「管理」をクリックしてから、下のような画面を開くことができます。

ここをクリックして、下のような画面を表示できますので「新しいトラッカー」をクリックして作成します。
ここでは、下のように今後作成するチケットの種類(カテゴリ)を作成します。

チケットのステータス

同様に、上部のメニューにある「管理」をクリックしてから、下のような画面を開くことができます。

同様に、赤枠の部分をクリックしてやると下のような画面が開けます。

これはすでに入力した後なのですが、こんな感じで、「ステータス(状態)」を登録することで、作成するチケットの状態を指定します。
つまりは、「このチケットは<ステータス>に対応した工程の作業」という意味です。

優先度

これも同様に、「選択肢の値」を選択します。

下のように、「新しい値」をクリックして追加してやります。

チケットの作成

チケットのトラッカーを追加してやると下のように、「新しいチケット」をクリックすることができます。これが見当たらないときは設定がたりない状態ということです。

これで、チケットを作成して、アイティアと行いたい作業が混同することを防げるであろう。
まぁこれ流行ってみないことにはどうしようもないな。。。

でわでわ。。。

Java Bag fix ~ArithmeticException in BigDecimal~

ArithmeticExceptionが出ました。

これは、割り切れない処理を行ったときにです用です。
JavaDocを参照すると「1 / 3」を行うと出力されるようです。

【実行コード】

BigDecimal three = new BigDecimal(1);
BigDecimal four = new BigDecimal(3);
System.out.println(three.divide(four));

【エラーログ】

java.lang.ArithmeticException: Non-terminating decimal expansion; no exact representable decimal result.

    at java.math.BigDecimal.divide(BigDecimal.java:1693)
    at jp.zenryoku.sample.statics.BigDecimalSample.test01(BigDecimalSample.java:14)
    at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
    at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62)
    at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
    at java.lang.reflect.Method.invoke(Method.java:498)
    at org.junit.platform.commons.util.ReflectionUtils.invokeMethod(ReflectionUtils.java:688)
    at org.junit.jupiter.engine.execution.MethodInvocation.proceed(MethodInvocation.java:60)

解決策

スケール指定と、まるめ算の指定を行うことで解決しました。

※注意スケールをセットしたら、そのBigDecimalクラスを返却します。

BigDecimal left = new BigDecimal(1);
BigDecimal right = new BigDecimal(3);
BigDecimal setScale = left.setScale(2);
BigDecimal res = setScale.divide(right, BigDecimal.ROUND_HALF_UP);
System.out.println(res.toString());

下のコードだとスケールを設定していない形になるのでエラーが出る。

BigDecimal left = new BigDecimal(1);
BigDecimal right = new BigDecimal(3);
left.setScale(2);
BigDecimal res = setScale.divide(right, BigDecimal.ROUND_HALF_UP);
System.out.println(res.toString());

Java Swing ゲーム作りや学習に画面をつくる

イントロダクション

Javaを学習して、基本文法がわかると今度は、Java API、つまりはListインターフェースなど既存のクラスを使用する事を学習すると思います。
ここら辺から、学習がうまく進まなくなる。自分がそうでした。。。

今思い返すと、何かしらのアプリケーションを作ってみるのが一番良いのだけど、「何かを作るほどの理解がない」「イマイチ自身が。。。」などと感じる人のもいるかもしれません。

しかし、まずはプログラミングを楽しみましょうという気持ちでSwingしてみませんか?
ちなみに、Jazzの世界ではカッコイイ演奏をすることを「Swingしてるねぇ!」といいます。関連は全くありません(笑)

ちなに、インスタンス=コンポーネントです。つまりラベルが表示されているというのはラベルがインスタンス化されているということです。
目に見えないインスタンスが目に見えマス。※目に見えないものもありますので注意が必要です。

Java Swing

Oracleのページでは、下のように書いていました。
Swingはグラフィカル・ユーザー・インタフェース(GUI)を構築し、豊富なグラフィック機能および双方向性をJavaアプリケーションに追加するコンポーネントのセットを実装しています。

Swingコンポーネントは、全体がJavaプログラミング言語により実装されています。プラガブルなルック・アンド・フィールにより、プラットフォーム間で同じ外観になるGUI、または現在のOSプラットフォーム(Microsoft Windows、Solaris(tm)、Linuxなど)のルック・アンド・フィールを想定したGUIが作成できます。

そして、チュートリアルもあるようです。

まずは、ここのチュートリアルを行い自分で学習していく土台を作っていくのも一つだと思います。

Swingの仕組み

Swingフレームワークでは、コンテナー(Container)にコンポーネント(Component)をセットして、それを表示するという仕組みを持っています。
単純に、画面上に「Hello World」と表示するプログラムを書くと下のようになります。

<プログラムコード1>

public class SwingMain {
    public static void main(String[] args) {
        JFrame frame = new JFrame();
        frame.getContentPane().add(new JLabel("Hello World."));
        frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
        frame.pack();
        frame.setVisible(true);
    }
}

処理内容

JFrame frame = new JFrame();の部分は、トップレベルコンテナーのJFrameクラスをインスタンス化しています。
そして、トップレベルコンテナーは以下の3つがあります。

JFrameについて

このうちJFrameクラスを使用するサンプルコードが良く見受けられます。上記のサンプルもこのJFrameを使用しています。
ちょっと長いですが、オラクルのドキュメントの説明が良いと思います。


Frame は、タイトルと境界線を持つトップレベル ウィンドウです。フレームのサイズには、ボーダー用に指定されたすべての領域が含まれます。この方法を使用して境界領域の寸法を得ることができるgetInsets。境界領域はフレーム全体のサイズに含まれているため、境界はフレームの一部を効果的に覆い隠し、サブコンポーネントのレンダリングおよび/または表示に使用できる領域を、左上隅の位置が 、 、および の長方形に制限し(insets.leftますinsets.top)。width - (insets.left + insets.right)のサイズは ですheight - (insets.top + insets.bottom)。
クラスのインスタンスとして実装されるフレームは JFrame、境界線やタイトルなどの装飾を持ち、ウィンドウを閉じるかアイコン化するボタン コンポーネントをサポートするウィンドウです。通常、GUI を使用するアプリケーションには、少なくとも 1 つのフレームが含まれます。アプレットもフレームを使用することがあります。
別のウィンドウに依存するウィンドウを作成するには (たとえば、別のウィンドウがアイコン化されると非表示になります)、dialogの代わりにa を使用しますframe.。別のウィンドウ内に表示されるウィンドウを作成するには、内部フレームを使用します。

まぁなんとなくの理解でもよいと思います。

コンポーネントの追加

frame.getContentPane().add(new JLabel("Hello World."));はラベル、テキストフィールド、ボタンなどのコンポーネントを追加する
時のコードです。つまり、このコードの引数にラベル以外のものもセットできるというわけです。

ちなみに、色付きのコードと下のコードは同じことを行っています。

Container cont = frame.getContentPane();
cont.add(new Label("Hello World."));

一行で書くか2行で書くかの違いです。細かいところでは、Containerのインスタンスを変数「cont」にセットしています。
複数回同じ処理をするのであれば、変数に取り出したインスタンスを使用するのがエコなコードになります。

処理の内容としては、以下の通りです。

  1. JFrameからコンテナーを取得する
  2. 取得したコンテナーにコンポーネント(ラベル)を追加する

閉じるときの処理設定

frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);画面を閉じるときの処理です。
のコードを書くことで画面を閉じるとアプリケーションが終わります。

単純に追加

単純にコンポーネントを追加すると下のようになります。
<プログラムコード2>

public class SwingMain {
    public static void main(String[] args) {
        JFrame frame = new JFrame();

        Container cont = frame.getContentPane();
        cont.add(new JLabel("Hello World."));
        cont.add(new JLabel("*************"));
        cont.add(new JLabel("**        **"));
        cont.add(new JLabel("**        **"));
        cont.add(new JLabel("**        **"));
        cont.add(new JLabel("*************"));

        frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
        frame.pack();
        frame.setVisible(true);
    }
}

<実行結果>

一番最後に追加したコンポーネントのみが表示されています。おそらく、コンテナーに一つのコンポーネントしか登録できないのでしょう。
なので、JPanelクラスを使用してコンポーネントを複数追加します。

JPanelを使う

上のコードを下の様に書き換えます。
<プログラムコード3>

public class SwingMain {
    public static void main(String[] args) {
        JFrame frame = new JFrame();

        Container cont = frame.getContentPane();
        JPanel panel = new JPanel();
        cont.add(panel);

        panel.add(new JLabel("Hello World."));
        panel.add(new JLabel("*************"));
        panel.add(new JLabel("**        **"));
        panel.add(new JLabel("**        **"));
        panel.add(new JLabel("**        **"));
        panel.add(new JLabel("*************"));

        frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
        frame.pack();
        frame.setVisible(true);
    }
}

<実行結果>

コードのように四角っぽいのを表示したかったのですが、横並びですね。。。

これは、デフォルトで設定されているレイアウトマネージャ(XXXLayoutクラス)がFlowLayoutになっているので、横並びになります。

BorderLayoutについて

デフォルトで設定されているレイアウトクラスBorderLayoutクラスです。下の図のようにレイアウトを組むことができます。
<レイアウト図1>

上のコードでは、レイアウトの指定(NORTH, CENTERなど)を使用していないので、余計変な形になっています。

レイアウトを使う

<プログラムコード4>

public class SwingMain {
    public static void main(String[] args) {
        JFrame frame = new JFrame("Window Test");
        frame.setLayout(new BorderLayout());

        Container cont = frame.getContentPane();
        cont.add(new JLabel("Hello World."), BorderLayout.NORTH);

        JPanel panel = new JPanel();
        panel.setLayout(new BoxLayout(panel, BoxLayout.Y_AXIS));
        panel.add(new JLabel("*************"));
        panel.add(new JLabel("**        **"));
        panel.add(new JLabel("**        **"));
        panel.add(new JLabel("**        **"));
        panel.add(new JLabel("*************"));
        cont.add(panel, BorderLayout.CENTER);

        frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
        frame.setSize(new Dimension(500, 200));
        frame.setVisible(true);
    }
}

BoxLayoutというクラスも出てきましたが、このクラスに関しては、さておきにして下のように表示されました。
<実行結果>

ポイントは、次のような部分です。

  1. JFrameのコンストラクターの引数に「Window Test」と追加したのでウィンドウのタイトル部分に表示されている。
  2. JPanelにBoxレイアウトを設定して、縦に追加する形を作ったので壮丁通りの表示になった。
  3. setSize()メソッドを使用して、JFrameのサイズを指定している。

レイアウトを色々試す

上記の表示コンポーネントに、いろいろと装飾を加えてみることにします。

コンポーネントにボーダーを入れてみる。

コンポーネント(表示している部品)に、ボーダーを入れるには次のメソッドを使用します。
JComponent#setBorder()を使用してボーダーを追加します。しかし表示しているJPanelのメソッドではありません。
それでも問題はありません。JavaDocを見ればわかるように、JPanelはJComponentのサブクラス(子供)になっています。
つまり、JComponentのメソッドを使用することができます。※privateのメソッドは使えません。

パネルにボーダーを入れる

プログラムコード4を修正して次のプログラムコード5を作成します。

<プログラムコード5>

public class SwingMain {
    public static void main(String[] args) {
        JFrame frame = new JFrame("Window Test");
        frame.setLayout(new BorderLayout());

        Container cont = frame.getContentPane();
        cont.add(new JLabel("Hello World."), BorderLayout.NORTH);

        JPanel panel = new JPanel();
        panel.setBorder(BorderFactory.createLineBorder(Color.black));
        panel.setLayout(new BoxLayout(panel, BoxLayout.Y_AXIS));
        panel.add(new JLabel("*************"));
        panel.add(new JLabel("**        **"));
        panel.add(new JLabel("**        **"));
        panel.add(new JLabel("**        **"));
        panel.add(new JLabel("*************"));
        cont.add(panel, BorderLayout.CENTER);

        frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
        frame.setSize(new Dimension(500, 200));
        frame.setVisible(true);
    }
}

実行結果は下の通りです。

ボーダーが真ん中らへんに1本入っただけです。。。これは、レイアウト図1のNORTHとCENTERにそれぞれ、ラベルとパネルを設定しているためNORTHとCENTERの境目にボーダーが票う辞された程度になっています。もっとわかりやすくほかのものも追加してみます。

<プログラムコード6>

public class SwingMain {
    public static void main(String[] args) {
        JFrame frame = new JFrame("Window Test");
        frame.setLayout(new BorderLayout());

        Container cont = frame.getContentPane();
        cont.add(new JLabel("Hello World."), BorderLayout.NORTH);

        JPanel panel = new JPanel();
        panel.setBorder(BorderFactory.createLineBorder(Color.black));
        panel.setLayout(new BoxLayout(panel, BoxLayout.Y_AXIS));
        panel.add(new JLabel("*************"));
        panel.add(new JLabel("**        **"));
        panel.add(new JLabel("**        **"));
        panel.add(new JLabel("**        **"));
        panel.add(new JLabel("*************"));
        cont.add(panel, BorderLayout.CENTER);

        JLabel west = new JLabel("東");
        west.setBorder(BorderFactory.createLineBorder(Color.BLUE));
        cont.add(west, BorderLayout.WEST);

        JLabel east = new JLabel("西");
        east.setBorder(BorderFactory.createLineBorder(Color.RED));
        cont.add(east, BorderLayout.EAST);

        JLabel south = new JLabel("南");
        south.setBorder(BorderFactory.createLineBorder(Color.YELLOW));
        cont.add(south, BorderLayout.SOUTH);

        frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
        frame.setSize(new Dimension(500, 200));
        frame.setVisible(true);
    }
}

上記のように、東西南北と中心の5つの領域にそれぞれのコンポーネントをセットしてやる形の表示ができました。

簡単なリファクタリング

今度はBoxLayoutを使用してみます。ここで、プログラムのコード量が増えてきたので、コードを分割してみようと思います。
コードを分割する手段として使えるのは「メソッドを使う」というところです。
具体的には次のようにやります。

1.メソッドの処理を言葉で表現する。

筆者の表現したものなので、「これが正解!」ってことではないので注意してください。それぞれの人にそれぞれの言葉があるように表現の仕方は無限大にあります

  1. JFrameをインスタンス化
  2. BorderLayoutをセット
  3. 北(NORTH)にJLabel「Hello World」をセット
  4. 中央にJPanelをセット
  5. 東。。。
  6. 西。。。
  7. 南。。。

※性格が出るんですね。。。

このように、東西南北、中央の5つに各描画処理を行っているので、そのように処理を分割しようと思います。
メソッド名は、わかりやすいとおもうので「中央を描く」「北を描く」。。。のようにつけることにします。

2.表現したように分割

東西南北、中央の5つに分割することにしたので、次のようなメソッドを作成しました。まだからの状態です。引数および返り値もありません。
そのため、ビルドエラーが出ます。説明の段階を踏むためなので、ご了承ください。
<プログラムコード7>

public class SwingMain {
    ...
    /** 北を描く */
    private void darwNorth() {
    }
    /** 中央を描く */
    private void darwNCenter() {
    }
    /** 西を描く */
    private void darwWest() {
    }
    /** 東を描く */
    private void darwEast() {
    }
    /** 南を描く */
    private void drawSouth() {
    }
}

そして、単純に既存のコードを移植します。
<プログラムコード8>

public class SwingMain {
    public static void main(String[] args) {
        SwingMain main = new SwingMain();
        JFrame frame = new JFrame("Window Test");
        frame.setLayout(new BorderLayout());

        Container cont = frame.getContentPane();
        main.darwNorth();
        main.darwNCenter();
        main.darwWest();
        main.darwEast();
        main.drawSouth();

        frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
        frame.setSize(new Dimension(500, 200));
        frame.setVisible(true);
    }

    /** 北を描く */
    private void darwNorth() {
        cont.add(new JLabel("Hello World."), BorderLayout.NORTH);
    }
    /** 中央を描く */
    private void darwNCenter() {
        JPanel panel = new JPanel();
        panel.setBorder(BorderFactory.createLineBorder(Color.black));
        panel.setLayout(new BoxLayout(panel, BoxLayout.Y_AXIS));
        panel.add(new JLabel("*************"));
        panel.add(new JLabel("**        **"));
        panel.add(new JLabel("**        **"));
        panel.add(new JLabel("**        **"));
        panel.add(new JLabel("*************"));
        cont.add(panel, BorderLayout.CENTER);
    }
    /** 西を描く */
    private void darwWest() {
        JLabel west = new JLabel("西");
        west.setBorder(BorderFactory.createLineBorder(Color.BLUE));
        cont.add(west, BorderLayout.WEST);
    }
    /** 東を描く */
    private void darwEast() {
        JLabel east = new JLabel("東");
        east.setBorder(BorderFactory.createLineBorder(Color.RED));
        cont.add(east, BorderLayout.EAST);

    }
    /** 南を描く */
    private void drawSouth() {
        JLabel south = new JLabel("南");
        south.setBorder(BorderFactory.createLineBorder(Color.YELLOW));
        cont.add(south, BorderLayout.SOUTH);

    }
}

この状態でもエラーが出ます。それは、Containerが宣言されているのは、メインメソッドだからです。

ならば、引数にContainerをわたしてやればよいのでわ?とおもうのでそのようにします。
<プログラムコード9>

package jp.zenryoku.swing;

import javax.swing.*;
import java.awt.*;

public class SwingMain {
    public static void main(String[] args) {
        SwingMain main = new SwingMain();
        JFrame frame = new JFrame("Window Test");
        frame.setLayout(new BorderLayout());

        Container cont = frame.getContentPane();
        main.darwNorth(cont);
        main.darwNCenter(cont);
        main.darwWest(cont);
        main.darwEast(cont);
        main.drawSouth(cont);

        frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
        frame.setSize(new Dimension(500, 200));
        frame.setVisible(true);
    }

    /** 北を描く */
    private void darwNorth(Container cont) {
        cont.add(new JLabel("Hello World."), BorderLayout.NORTH);
    }
    /** 中央を描く */
    private void darwNCenter(Container cont) {
        JPanel panel = new JPanel();
        panel.setBorder(BorderFactory.createLineBorder(Color.black));
        panel.setLayout(new BoxLayout(panel, BoxLayout.Y_AXIS));
        panel.add(new JLabel("*************"));
        panel.add(new JLabel("**        **"));
        panel.add(new JLabel("**        **"));
        panel.add(new JLabel("**        **"));
        panel.add(new JLabel("*************"));
        cont.add(panel, BorderLayout.CENTER);
    }
    /** 西を描く */
    private void darwWest(Container cont) {
        JLabel west = new JLabel("西");
        west.setBorder(BorderFactory.createLineBorder(Color.BLUE));
        cont.add(west, BorderLayout.WEST);
    }
    /** 東を描く */
    private void darwEast(Container cont) {
        JLabel east = new JLabel("東");
        east.setBorder(BorderFactory.createLineBorder(Color.RED));
        cont.add(east, BorderLayout.EAST);

    }
    /** 南を描く */
    private void drawSouth(Container cont) {
        JLabel south = new JLabel("南");
        south.setBorder(BorderFactory.createLineBorder(Color.YELLOW));
        cont.add(south, BorderLayout.SOUTH);

    }
}

実行結果は、下のようになります。

はじめに表示したものと同じです。そうならなくてはなりません。リファクタリングした後は、処理結果が全く同じになる事が大切です。
なので、単体テストクラスがないとリファクタリングするのがとても大変になリます。
逆に単体テストクラスがない場合はリファクタリングができません。

HTMLを読み込む

単純にHTMLファイルを作成して、それをSwingで表示するということができます。

<プログラムコード10>

    public static void main(String[] args) {
        SwingHtml main = new SwingHtml();
//        JEditorPane editorPane = new JEditorPane("text/html", HTML);
        JEditorPane editorPane = null;
        try  {
            URL url = new File("PracticeJava1/resources/012.html").toPath().toUri().toURL();
            editorPane = new JEditorPane(url);
        } catch (Exception e) {
            e.printStackTrace();
            System.exit(-1);
        }
        editorPane.setEditable(false);
        editorPane.setPreferredSize(new Dimension(200, 150));
        JScrollPane scrollPane = new JScrollPane(editorPane);

        JPanel panel = new JPanel();

        panel.setLayout(new BorderLayout());
        panel.add(scrollPane, BorderLayout.CENTER);
        main.getContentPane().add(panel);
        main.setSize(new Dimension(400, 300));
        main.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
        main.setVisible(true);
    }

Java Swing テキストRPGを作る ~Swingを使って画面を作る~

イントロダクション

テキストRPGを作成しようと思います。どちらかというとリメイクに近いのですが、前回作成したものが完成していないので。。。
兎にも角にも、Java Swingでの実装になりますので、クラスの扱い方の学習がしやすい、かつ、視覚的にクラス関係を理解できると思います。

IDE(開発ツール)はBlueJを使用しています。

Swingを使って画面を作る

以前、テキストRPGを作成しました。
Gitにアップしてあります。

しかし、これはコマンドプロンプト上で実行するもので「ゲーム」って感じのしないものでした。画面のリロードとかうまくいきません。。。
なので、いろいろと考えた末、Java Swingで実装したらよいと考えなおしました。

余談

実際、筆者はJavaのオブジェクト指向プログラミングという部分をSwingで学びました。つまり、クラスの扱い方を理解しました。
「オブジェクト指向」という言葉を使うと「staticおじさん」や「オブジェクト指向おじさん」よろしく。。。混沌の世界に足を踏み入れることになるので言葉を変えていきたいと思います。

クラスの扱い方を理解する

まとめると、筆者はSwingの実装を通してクラスの扱い方を理解しました。というところを言いたかった次第です。
そして、最近覚えたBlueJを使用して、テキストRPGを作成していきたいと思います。

画面を作る

Swingを使用して、画面を作成していきます。まずは、テキストRPGを実行して「テキスト」を表示する領域が必要になります。

初めのコード

この領域には次の機能が必要になります。

  1. 文字を表示する
  2. 文字をクリア(削除)する

これらを実現するためにプログラム的には、以下のものを使用します。

  • JFrame: アプリの表示する領域フレームを表現するクラス
  • JPanel: フレーム上にコンポーネントを配置するパネル
  • 各種コンポーネント: ラベル(JLabel)、テキストエリア(JTextArea)など

クラス継承について

クラスの継承関係を見てみるとわかりやすいです。

これは、JFrameクラスの親は、Frameクラス、そしてその親は。。。とそれぞれの継承関係を示しています。
つまり、クラスキャストも行うことができるということです。

JFrame frame = new JFrame();
Frame superFrame = (Frame) frame;
superFrame.XXXX;

言葉を変えると、親クラスが必要な時は、上記のようにキャストして使用することができます。
そして、親クラスのメソッドを呼び出すこともできます。

JFrame frame = new JFrame();
frame.addNotify(); // java.awt.Frameのメソッド

コードについて

作成したコードは、下のような表示を行います。どの部分がフレームなのか?も記述しました。

コード

TextRPGMainクラスは、JFrameを継承しているところに注意してください。

package jp.zenryoku.rpg;

import javax.swing.JFrame;
import javax.swing.JTextArea;
import javax.swing.JPanel;
import javax.swing.JLabel;
import java.awt.Container;
import java.awt.BorderLayout;
import java.awt.Dimension;
import java.awt.Toolkit;

/**
 * クラス TextRPGMain の注釈をここに書きます.
 * 
 * @author (Takunoji)
 * @version (1.0)
 */
public class TextRPGMain extends JFrame
{
    public static void main(String[] args) {
        // JFrameを継承しているのでJFrameクラスのメソッドを使える
        TextRPGMain main = new TextRPGMain();
        main.run("Text RPG");
    }

    public void run(String title) {
        // タイトルをセット
        setTitle(title);
        // 画面の表示位置と画面サイズをセット
        Dimension windowSize = Toolkit.getDefaultToolkit().getScreenSize();
        int xPos = (int) windowSize.getWidth() / 4;
        int yPos = (int) windowSize.getHeight() / 4;
        setBounds(xPos, yPos, xPos * 2, yPos * 2);
        // 画面を閉じたときアプリを終了する設定
        setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);

        // ラベル
        JLabel titleLabel = new JLabel("Text RPG");
        JTextArea textarea = new JTextArea();
        // テキストエリア
        textarea.setColumns(40);
        textarea.setRows(10);

        // ラベル用のパネル
        JPanel titlePanel = new JPanel();
        titlePanel.add(titleLabel);

        // テキストエリア用のパネル
        JPanel textPanel = new JPanel();
        textPanel.add(textarea);

        // パネルをセットするコンテナ
        Container contentPane = getContentPane();
        // コンテナにパネルをセット
        contentPane.add(titlePanel, BorderLayout.NORTH);
        contentPane.add(textPanel, BorderLayout.CENTER);
        // 表示する設定
        setVisible(true);
    }
}

ちなみに、クラス図で見ると下のようになります。

メインメソッドを持っているクラスのみになります。

次は、クラスの拡張実装を行ってみようと思います。

クラス継承の実装

クラスの継承方法は下のように「extends クラス名」と書くだけです。

public class ChildClass extends ParentClass {
   ....
}

JLabelを拡張する

「拡張」という言葉に戸惑うかもしれません。ズバリ「JLabelを継承して新しいクラスを作成する」という意味です。

新しく「TitleLabel」クラスを追加します。このクラスは上記のTextRPGMainクラスのrun()メソッドで行っている処理を少なくするように実装しています。
別な言い方をすると「タイトルラベルの処理はTitleLabelに任せましょう。というところです。

では、どのようになるのか?というところです。

TitleLabelの実装

  1. TitleLabelクラスを作成します。
  2. JLabelを継承します。
  3. 現状はコンストラクタの実装のみで事足ります。

実際のコードです。

package jp.zenryoku.rpg;

import javax.swing.JLabel;
import java.awt.Dimension;
import java.awt.Color;

/**
 * クラス TitleLabel の注釈をここに書きます.
 * JLabelを拡張して、テキストRPGのタイトルをセットするラベルを作成する。
 * 
 * @author (Takunoji)
 * @version (1.0)
 */
public class TitleLabel extends JLabel
{
    public TitleLabel(String title, Dimension windowSize) {
        super(title);
        int width = (int) windowSize.getWidth() / 4;
        int height = (int) windowSize.getHeight() / 16;
        Dimension labelSize = new Dimension(width, height);
        setOpaque(true);
        setPreferredSize(labelSize);
        setBackground(Color.GREEN);
        setHorizontalAlignment(JLabel.CENTER);
    }
}
  1. JLabelを継承しているので、親クラス(JLabel)のコンストラクタを呼び出します。super(title);
  2. ラベルのサイズ(縦横の幅指定)をします。
  3. ラベルの領域がわかるように、緑色の背景を付けます。

ちなみに、ラベルのサイズは、毎回値を変更するのは、面倒なのでPCの画面サイズに合わせてサイズを変更するように実装しました。

そして、run()メソッドと今回作成したクラスの処理の関係を示します。

TextRPGMain#run()

JLabelを生成して、タイトルをセットしただけで、幅や背景などはセットしていませんでした。

// ラベル
JLabel titleLabel = new JLabel("Text RPG");

なので、この「TitleLabel」クラスを作成していなかったらTextRPGMainクラスにJLabelの処理を書くことになります。
このTextRPGMainクラスにJLabelの処理を書くことがプログラム的に美しくないのでTitleLabelを作成しタイトルラベルのことはこのクラスにコードを書きましょう。という風に考えてプログラムを作りました。

TextRPGMain#run()の修正

ズバリ下のように修正しました。1行です。
JLabel titleLabel = new JLabel("Text RPG");TitleLabel titleLabel = new TitleLabel("Text RPG", windowSize);
になりました。表示の結果は以下の通り
<元々の表示>

<修正後の表示>

次は、プログラム・コードを見てみましょう。
<元々の処理>

public void run(String title) {
    // タイトルをセット
    setTitle(title);
    // 画面の表示位置と画面サイズをセット
    Dimension windowSize = Toolkit.getDefaultToolkit().getScreenSize();
    int xPos = (int) windowSize.getWidth() / 4;
    int yPos = (int) windowSize.getHeight() / 4;
    setBounds(xPos, yPos, xPos * 2, yPos * 2);
    // 画面を閉じたときアプリを終了する設定
    setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);

    // ラベル
    JLabel titleLabel = new JLabel("Text RPG");
    // テキストエリア
    JTextArea textarea = new JTextArea();
    textarea.setColumns(40);
    textarea.setRows(10);

    // ラベル用のパネル
    JPanel titlePanel = new JPanel();
    titlePanel.add(titleLabel);

    // テキストエリア用のパネル
    JPanel textPanel = new JPanel();
    textPanel.add(textarea);

    // パネルをセットするコンテナ
    Container contentPane = getContentPane();
    // コンテナにパネルをセット
    contentPane.add(titlePanel, BorderLayout.NORTH);
    contentPane.add(textPanel, BorderLayout.CENTER);
    // 表示する設定
    setVisible(true);
}

<修正後>

public void run(String title) {
    // タイトルをセット
   setTitle(title);
    // 画面の表示位置と画面サイズをセット
    Dimension windowSize = Toolkit.getDefaultToolkit().getScreenSize();
    int xPos = (int) windowSize.getWidth() / 4;
    int yPos = (int) windowSize.getHeight() / 4;
    setBounds(xPos, yPos, xPos * 2, yPos * 2);
    // 画面を閉じたときアプリを終了する設定
    setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);

    // ラベル
    TitleLabel titleLabel = new TitleLabel("Text RPG", windowSize);
    // テキストエリア
    JTextArea textarea = new JTextArea();
    textarea.setColumns(40);
    textarea.setRows(10);

    // ラベル用のパネル
    JPanel titlePanel = new JPanel();
    titlePanel.add(titleLabel);

    // テキストエリア用のパネル
    JPanel textPanel = new JPanel();
    textPanel.add(textarea);

    // パネルをセットするコンテナ
    Container contentPane = getContentPane();
    // コンテナにパネルをセット
    contentPane.add(titlePanel, BorderLayout.NORTH);
    contentPane.add(textPanel, BorderLayout.CENTER);
    // 表示する設定
    setVisible(true);
}

こんな感じです。
次は、テキストエリアをタイトルラベルと同じように拡張しようと思います。

JTextAreaの拡張

まずは、現状のクラス作成状況を確認します。

次は、画面の白い部分「テキストエリア」を拡張して文字列の表示領域を作成します。

今回も、テキストエリアを担当するクラスを作成します。ネーミングセンスが問われますが、目的と役割を明確にすることを最優先にするので。。。

RpgTextクラスを作成

RpgTextAreaクラスとします。作成はまずJTextAreaを継承します。

import javax.swing.JTextArea;

/**
 * クラス RpgTextArea の注釈をここに書きます.
 * テキストRPGの表示する文字列をこの領域に出力(描画)する。
 * 背景は黒、イメージはドラ○エのような感じにしたい。
 * 
 * @author (Takunoji)
 * @version (1.0)
 */
public class RpgTextArea extends JTextArea
{
    public RpgTextArea() {

    }
}

そして、テキストの表示を担当するので、メインクラスに書いている次の部分が不要になります。

JTextArea textarea = new JTextArea();
textarea.setColumns(40);
textarea.setRows(10);

同様に、次のようにコードをRpgTextAreaに追加します。

public class RpgTextArea extends JTextArea
{
    public RpgTextArea() {
        setColumns(40);
        setRows(10);
    }
}

そして、TextRPGMain#run()を修正

    public void run(String title) {
        setTitle(title);
        Dimension windowSize = Toolkit.getDefaultToolkit().getScreenSize();
        int xPos = (int) windowSize.getWidth() / 4;
        int yPos = (int) windowSize.getHeight() / 4;
        setBounds(xPos, yPos, xPos * 2, yPos * 2);
        setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);

        TitleLabel titleLabel = new TitleLabel("Text RPG", windowSize);
        RpgTextArea textarea = new RpgTextArea();

        JPanel titlePanel = new JPanel();
        titlePanel.add(titleLabel);

        JPanel textPanel = new JPanel();
        textPanel.add(textarea);

        Container contentPane = getContentPane();
        contentPane.add(titlePanel, BorderLayout.NORTH);
        contentPane.add(textPanel, BorderLayout.CENTER);

        setVisible(true);
    }

JPanelをRpgTextAreaに修正、不要なコードを削除しました。

この状態でプログラム実行すると下のようになります。

全く変わりません。その代わり、run()メソッドのコードの量は(少しですが)減りました。
ここから、テキストエリアのおしゃれをしていきます。
参照するのはJavaDocのJTextAreaです。
他にも次のクラスを参照します。

Fontクラスを見ると、フォントファイルを指定することでオリジナルのフォントも使えるようです。

<実行結果>

TextAreaのサイズ設定

画面のサイズ指定に関して、文字入力することも考えてPCの画面サイズから文字の数、行の数を設定するようにプログラムを組みました。
理論的なところが、はっきりと決まらなかったのですが、縦横の「~分の~」という形で実装しました。

public class RpgTextArea extends JTextArea
{
    /** コンストラクタ */
    public RpgTextArea(Dimension size) {
        int widthCol = (int) size.getWidth() / 19;
        int heightRow = (int) size.getHeight() / 28;
        System.out.println("width: " + widthCol);
        System.out.println("height: " + heightRow);
        setColumns(widthCol);
        setRows(heightRow);
        ...
    }
    ...
}

実行結果は、下のような形です。

とりあえずは、これで、画面が作成できたのでここでひと段落になります。

まとめ

クラスを継承すると親クラスのメソッドなどは、自分のクラス内のメソッドのように使用することができる。
なので、下のようなコードが書ける。

public class RpgTextArea extends JTextArea
{
    /** コンストラクタ */
    public RpgTextArea(Dimension size) {
        int widthCol = (int) size.getWidth() / 19;
        int heightRow = (int) size.getHeight() / 28;
        System.out.println("width: " + widthCol);
        System.out.println("height: " + heightRow);
        setColumns(widthCol);
        setRows(heightRow);
        // 背景の描画準備
        setOpaque(true);
        // フォントの設定
        setFont(createTextFont());
        // 背景を黒にする
        setBackground(Color.BLACK);
        // 白い文字の設定
        setForeground(Color.WHITE);
        // 白いボーダーの設定
        Border border = BorderFactory.createLineBorder(Color.GREEN);
        setBorder(BorderFactory.createCompoundBorder(border,
            BorderFactory.createEmptyBorder(10, 10, 10, 10)));

        setWrapStyleWord(true);
        setLineWrap(true);
    }
    ....
}

つまるところは、親クラスのpublic, (packaged, )protectedのメソッドを子クラスが使用することができるので「setXXX」のようなメソッドを直接呼び出すことができる。

今回は、コンストラクタのみを使用した形で実装しました。
次は、テキストの表示などを行っていきたいと思います。

次回 >>>