備忘録

何かあったとき用に

家具作った

計5時間ぐらい (片付け含めると6時間ちょい) かかったので昼飯は16時ぐらいに食べた. (作業開始が10時くらいなので)

以上.

Nuxt2 + TypeScriptの環境構築

この記事はKaigen Discord Advent Calendar 2018 13日目の記事です。

前置き

Nuxt.jsでTypeScriptを導入した環境を構築するのにかなり苦労したので同じことをしようと思う人と自分に向けてまとめておく。
最終的なテンプレートはGitHubにあげていますのでご参考に。

環境

  • Node.js: 10.14.1
  • Nuxt: 2.3.4

構築過程

プロジェクト作成

まず

$ yarn create nuxt-app

でNuxtプロジェクトを作る。構成は以下の画像のようにした。

f:id:KashEight:20181206205904p:plain

今回は環境構築だけなのでサーバサイドフレームワークやUIフレームワークは使わず, また, APIをいじいじするわけでもないのでAxiosも入れない。Universalアプリケーション作るわけでもないのでSPA。lintはかけたいのでeslintやprettierは今回のプロジェクトで導入した。

プロジェクトを作成したらとりあえず

$ yarn run dev

で動かしてみる。
Nuxt v2.3.4ではこのような画面が出たら成功。

f:id:KashEight:20181206213350p:plain

TypeScriptの導入

TypeScriptとWebpack用のloaderとnodeの型宣言ファイルを導入する

$ yarn add -D typescript ts-loader @types/node

Nuxt(というよりかはVueファイル)で使用するパッケージを導入

$ yarn add vue-class-component vue-property-decorator vuex-class

vue-class-componentやvue-property-decoratorをベースとしたnuxt-class-componentnuxt-property-decoratorがnuxt-communityで公開されているがどちらともメンテナンスが滞ってる(nuxt-property-decoratorは最近メンテナンスが再開された?)し, どちらともベースとなるもので事足りるのとそっちの方が機能が多いので別に導入しなくていいと思う。

TypeScriptをビルドできるようにする

TypeScriptをビルドできるようにWebpackの設定をいじってみる。
Nuxtではnuxt.config.jsのbuildプロパティ*1でWebpackの設定をいじるほか, モジュール*2という形でもWebpackの設定をいじることができる。
前者の方法では軽く設定を変える程度で複数プロジェクトで同じようなことをしない場合, そうではない場合は後者の方法を用いるのがいいでしょう。
今回のような場合は後者の方法で実装したほうがよさそう。
幸いにもNuxt2ではないがTypeScriptのテンプレートがあったのでそれを参考にNuxt2用に実装してみる。
まず, modules/typescript.jsとmodulesフォルダとjsファイルを作り以下のコードを記入する。(一応, tsxにも対応できるようにした)

module.exports = function() {
    // Nuxtで.ts/.tsxファイルを解決させる
    this.nuxt.options.extensions.push('ts', 'tsx')
    // Webpackの設定
    this.extendBuild(config => {
        // .ts/.tsxをビルドできるようにts-loaderを追加する。
        config.module.rules.push({
            test: /\.tsx?$/,
            exclude: /(node_modules)/,
            loader: 'ts-loader',
            options: {
                appendTsSuffixTo: [/\.vue$/]
            }
        })
        // Webpackのextensionsに.tsが入っていない場合解決できるようにする
        if (!config.resolve.extensions.includes('.ts')) {
            config.resolve.extensions.push('.ts')
        }
        // Webpackのextensionsに.tsxが入っていない場合解決できるようにする
        if (!config.resolve.extensions.includes('.tsx')) {
            config.resolve.extensions.push('.tsx')
        }
    })
}

次にモジュールを読み込めるようにnuxt.config.jsに以下を追記する。

modules: ['~modules/typescript.js']

tsconfig.jsonは以下のようにした。

{
    "compilerOptions": {
        "target": "es5",
        "lib": ["dom", "es2015", "es2016", "es2017"],
        "module": "es2015",
        "moduleResolution": "node",
        "alwaysStrict": true,
        "experimentalDecorators": true,
        "noImplicitAny": false,
        "noImplicitThis": true,
        "strictNullChecks": true,
        "sourceMap": true,
        "removeComments": true,
        "suppressImplicitAnyIndexErrors": true,
        "allowSyntheticDefaultImports": true,
        "allowJs": true,
        "baseUrl": ".",
        "paths": {
            "~/*": ["./*"],
            "@/*": ["./*"]
        },
        "typeRoots": [
            "node_modules/@types",
            "types"
        ]
    },
    "exclude": [
        "node_modules"
    ]
}

vscodeだと以下のようなファイルエラーが出ますが無視して構いません。(理由はよくわからないです…分かる人は教えてくれれば幸いです。)

[ts] 入力ファイルを上書きすることになるため、ファイル <rootPath>/modules/typescript.js' を書き込めません。

2/20追記: コメントにて

tsconfing.jsonのcompilerOptionsにoutdirかoutFileを指定するとエラーがなくなるようです。
変換しない場合は、"outFile": "" とするかまたは存在しないデタラメなフォルダを指定すればいいようです。

という指摘をいただきました。
確認はまだしていないのですがこれでそのvscodeのエラーは回避できるようです。
7ccさんありがとうございます!

最後に, VueでTypeScriptが使用できるように型宣言ファイルをtypes/index.d.tsのように作成して以下を記入する。

declare module '*.vue' {
    import Vue from 'vue'
    export default Vue
}

TypeScriptを使用する

pages/index.vueでTypeScriptを使用してみる。ソースは以下のようにした。

<template>
  <section class="container">
    <div>
      <logo/>
      <h1 class="title">
        {{ title }}
      </h1>
      <h2 class="subtitle">
        {{ fuwa }}
      </h2>
      <div class="links">
        <a
          href="https://nuxtjs.org/"
          target="_blank"
          class="button--green">Documentation</a>
        <a
          href="https://github.com/nuxt/nuxt.js"
          target="_blank"
          class="button--grey">GitHub</a>
      </div>
    </div>
  </section>
</template>

<script lang="ts">
import Vue from 'vue'
import Component from 'vue-class-component'
import Logo from '~/components/Logo.vue'

@Component({
    components: {
        Logo
    }
})
export default class extends Vue {
    title = 'hoge'
    desc = 'fuwa'
}
</script>

<style>
.container {
  min-height: 100vh;
  display: flex;
  justify-content: center;
  align-items: center;
  text-align: center;
}

.title {
  font-family: 'Quicksand', 'Source Sans Pro', -apple-system, BlinkMacSystemFont,
    'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif;
  display: block;
  font-weight: 300;
  font-size: 100px;
  color: #35495e;
  letter-spacing: 1px;
}

.subtitle {
  font-weight: 300;
  font-size: 42px;
  color: #526488;
  word-spacing: 5px;
  padding-bottom: 15px;
}

.links {
  padding-top: 15px;
}
</style>

ビルドしてみる

一度, yarn run lint --fixyarn run devをしてビルドしてみよう。
localhost:3000に接続してみて以下の画像のようになったら成功。

f:id:KashEight:20181207011810p:plain

これでTypeScriptの導入は終了した。

TypeScriptにlintをかける

TypeScriptを導入したはいいけれども.ts/.tsxファイルや.vueファイル内のscript部分にはlintはかからない。
そこでtslintやtypescript-eslint-parserを導入してlintをかけようと思う。今回はeslintを導入しているのとtslintとvueの相性が悪いのでtypescript-eslint-parserを使ってlintをかける。
まず必要となるパッケージを追加する。

$ yarn add -D typescript-eslint-parser eslint-plugin-typescript

nuxt.config.js

config.module.rules.push({
    enforce: 'pre',
    test: /\.(js|vue)$/,
    loader: 'eslint-loader',
    exclude: /(node_modules)/
})

を以下のように変更

config.module.rules.push({
     enforce: 'pre',
     test: /\.(js|ts|vue)$/,
     loader: 'eslint-loader',
     exclude: /(node_modules)/
})

.eslintrc.jsに以下を追記

overrides: [
    {
        files: ["*.ts", "*.vue"],
        parserOptions: {
            parser: "typescript-eslint-parser"
        },
        plugins: ['vue', 'prettier', 'typescript']
    }
]

これでTypeScriptにlintがかかるようになった。

余談

この記事を作るきっかけとしてyarn create nuxt-appでeslintとprettierを導入して初期の状態でビルドするとエラーを吐いて導入できなかったのがあった。
原因は.vueファイル内のscriptの言語がTypeScriptだと初期のeslintのパーサであるbabel-eslintで解析してしまうためそれを回避するために記事中に書いたTypeScriptにlintをかけるような方法を取らなければいけなかった。
この記事を書いてる最中にNuxtのバージョンが上がったため新しいバージョンで試したところ, エラーを吐かずにすんなりビルドを通した。
今までの苦労はなんだったんだろう…。

脚注

楽しいHypixel: PaintBall編

この記事はKaigen Discord Advent Calendar 5日目の記事です。 PaintBall編とか言ってるけど続くかはわかりません。

前書き

この記事はHypixelのPaintBallの魅力と楽しみ方の一つを自分なりに伝えるための記事なのでそんな本格的なことは書かないつもりです。
あと(こんな紹介記事書いてるけど)別にPaintBallが上手いわけではないのでKillstreakの選び方やPerkの選び方などは参考程度に留めておいてください。

PaintBallってなんぞや

(知ってる人は飛ばして構いません)
ゲームルールは

"2チームに分かれて雪玉投げあって相手のチームの残機を先に0にしたチームの勝利"

…これだけ見るとただの雪合戦と何が違うのか, そしてなんの面白さがあるのかわかりにくいかもしれません。
普通のPaintBallならつまらないでしょうけどここはあの"Hypixel", 普通ではないんです。
じゃあ何が違うのか?説明していきましょう。

豊富なShop

まずはHypixel恒例のShopを見ていきましょう。
Classic Games Lobbyへ行きPaintBallのShopを覗いてみます。 f:id:KashEight:20181201020354p:plain
5つの項目がありますが特に重要なのは上の3つです。それぞれ見ていきましょう。

Hat

f:id:KashEight:20181201020642p:plain
(テクスチャバグっててごめんなさい)
見る限りたくさんあって頭(Hatだけに)が混乱するかと思いますがこれらは大きく2種類にわけることができます。
一つは安価で購入できるBlockHat, もう一つは高価なSkullHatです。(BlockHat, SkullHatはそれぞれわかりやすくするために自分がつけた名称なので正しい名称ではないのでご注意を)
この二つの説明の前にまずはHatの説明をしましょう。

Hatとは

Hatは購入して装着することで試合にてHatに応じた効果を得ることができます。ただエフェクトを出す(意味のないネタ)Hatやゲームに大きな影響をもたらすHatまで多種多様なHatがこのPaintBallでは存在しており, ゲームをプレイする上では必要不可欠な存在です。
Hatを装着するにはShopで, 購入したHatをクリックするだけで装着, 次の試合から効果を得られます。逆に言えば装着するまでその効果は得られないということです。
勿論装着できるHatは一人一つまで, でないとゲームバランスは崩壊するからね。(元から壊れてるとか言わない)

このHatの量を見て大抵の方は「けどこんなたくさんの中から一つとか選べないよぉ~><」ってなると思います。
そこでBlockHat, SkullHatと分け, それぞれの中で必要なものをピックアップしましょう。まずはこの二種類の説明から。

BlockHatはブロック型のHatで安価(4,200コイン)で購入できるものです。コインがない序盤に購入しやすいです。
一方, SkullHatはプレイヤーの頭の形をしたHatでかなり高価(50,000/75,000コイン)です。クールタイムがありますがその分価値にあった性能を出してくれるのでコインが溜まったら買うのが良いと思います。

次にこの二種類の中から必要なものを紹介します。

BlockHat

  • Speed Hat
    -> 常時Speed2が付与, PaintBallのマップは広いので速く移動できるのは試合の勝敗を大きく分けます。

BlockHatはこの一つだけで十分です。というかこれを選ばないでどう試合すればいいかレベルなので必ず買いましょう。

SkullHat

  • Rezzus Hat
    -> スニークすると5秒間雪玉にホーミング性能がつく, クールタイムは70秒
  • Paintballkitty Hat
    -> 常時Speed2が付与, スニークすることで5秒間Speed3が付与される, クールタイムは70秒

SkullHatは基本的に高いのもあって最初のうちは手を出すことは難しいかもしれません。最初のうちはSpeed Hatでプレイしてコインに余裕が出てきて買おうと思ったらこの二つのどちらかを買えばいいと思います。
私のオススメは二番目のPaintballkitty Hatです。Speed Hatの上位互換版なのでHatを変えても違和感は全く無いです。
Rezzus HatはSpeed2は付与されませんが雪玉のホーミング性能が最強です。狭いマップではこれに敵うHatはないでしょう。

Perk

f:id:KashEight:20181201030640p:plain
Hatとは違って一気に数が少なくなりました。
けれどもPaintBallの中で一番重要な部分であり一番コインを必要とするのがこのPerkです。
ではこのPerkとはなにか, そしてどれを選べばいいのかこれから説明します。

Perkとは

Perkはレベル制となっておりHatとは違って買い切りではありません。
レベルの上限はそれぞれのPerkによって違い, 短いもので5レベル, 長いもので50レベルまであります。
全部上限まで開放するのはかなりの時間をPaintBallに捧げる必要はあるので現実的ではありません, そこでこの中からHatのように必要なものをピックアップしましょう。

  • Fortune
    -> 敵をKillした際にKillstreakをさらに1個貰える確率が増える。一レベル上げるごとに5%増加。上限は20レベル(100%)
  • Superluck
    -> 敵をKillした際に雪玉を10個貰える確率が増加する。一レベル上げるごとに5%増加。上限は20レベル(100%)
  • Headstart
    -> 初めから持っているKillCoinを増やすことができる。一レベル上げるごとに1KillCoinを得る。上限は5レベル(5KillCoins)

他にもGodfatherやEnduranceがありますがとりあえずこの三つを上限にすることを目標にすればいいかと思います。
どれを先に上げるかですが自分はFortune, Headstart, Superluckの順にあげるのがおすすめです。どのPerkにも言えることですが買うごとに値段は上昇していくため最適解としては

Fortune(途中まで) -> Headstart(3か4レベルまで) -> Superluck -> Fortune(最後まで)

が良いかもしれませんね。

Killstreak

f:id:KashEight:20181201034039p:plain
数が多くなりましたが, これも同じように説明していきましょう。

Killstreakとは

敵をKillしたときにもらえるKillCoinを使って使用できるスキルです。雪玉を遠くに速く投げることができるKillstreakから敵を全滅させるKillstreakまで色々なKillstreakが存在します。
ちなみに, 先程の画像以外にも初めから所持しているKillstreakもあるので画像が全てのKillstreakというわけではないです。
試合で使えるKillstreakをピックアップしていきましょう。

  • Strong Arm
    -> 30秒間雪玉の飛ぶ速度が早くなり, 遠くに飛ぶようになる。消費KillCoinは4, しかも初めから所持している。
  • Triple Shot
    -> 30秒間投げた雪玉が3方向に飛ぶようになる。Strong Armと併用可。消費KillCoinは3
  • Nuke
    -> カウントダウンが始まり5秒後に「リスポーン時の無敵時間中以外」の敵を全員Killする。Killした敵一人ごとに1KillCoin手に入る。クールタイムは60秒, 消費KillCoinは25
  • +10
    -> 味方の残機を10増やす。初めから使える+3(消費KillCoinは5)の上位互換, これで勝敗が大きく分かれることがある。消費KillCoinは15
  • Force Field
    -> 15秒間無敵になる。コストが高いがその分敵をKillすれば元は取れる。ただしKillstreakを使った攻撃(TNT Rain, Revengeなど)には無敵はきかない。クールタイムは60秒?(情報がないのでわからない), 消費KillCoinは50

マップにもよるけどほとんどのマップで使えるKillstreakを列挙しました。これ以外にもLightningやEnder Pearlと言ったマップによってはかなり強いKillstreakもあるので適宜使い分けよう。特にNukeは狭いマップやプレイヤーが少ない試合では効果を発揮できないことが多いです。

豊富なマップ

HypixelのPaintBallのマップは少人数用から大人数用まで色々なマップが存在しています。
マップは人の好みが分かれますがとりあえずの参考として自分の好きなマップを上げたいと思います。

  • Mansion
    -> 少人数マップ, かなり狭いのもあってLightningが大いに活躍できる。
  • Herobrine
    -> 大人数マップ, リスキル等でここで100Killを叩き出すことはよくある。自分も何回か出した。
  • Juice
    -> 少人数マップ, Lightningが活躍できるので楽しい。
  • Victorian
    -> 大人数マップ, Herobrineと同じくリスキルができる。

初心者のうちはマップに慣れないだろうけど慣れてきたらどのマップに自分は合ってるかわかるので是非ともいろんなマップに挑戦していただきたい。
まずは試合でKill数1位を目指して頑張ろう!

まとめ

PaintBallは初心者には取っ付きにくいですがコツさえ掴めば誰でもできるものです。
自分も最初はKillされてばかりでしたがやっていくうちに逆にKillをする方になっていたのでどんなことにも当てはまりますが徐々に慣れていくことが大切だと思います。
もしPaintBallに興味を持ったならば是非とも参加してみてください!というかしてほしい, ただでさえ人が少ないから。

PySide2で特定の子ウィジェットを取得し, 処理する

ただ単純に子ウィジェットを取得するにはchildren()関数を使えばいいのだが, 特定の子ウィジェットを探し出し処理するのにけっこう骨が折れた。
何通りか試していい答えが出たので試した通りを例に上げて忘れないうちに書いておく。

その0: 下準備

下準備としてこういうコードを用意した(長いのでgistにしました):

gist.github.com

これを実行させてみるとこういう感じの画面となる
f:id:KashEight:20180729015844p:plain

ここからボタン:"button2"だけを取得してテキストを変えてみようと思う。

その1: findChildren()関数を使う

目的の関数を探す

まず始めにそのクラスの情報を見るためにPySide2の公式リファレンスを確認した。
全部のWidget/LayoutクラスにはベースとなるQtWidgets.(QWidget|QLayout)クラスが存在するのでそのクラスを見ることにした。
QtWidgets.QWidgetにはchildAt()という関数が存在したけれども, 詳細を見ると

Parameters:
x – PySide2.QtCore.int
y – PySide2.QtCore.int Return type:
PySide2.QtWidgets.QWidget

Returns the visible child widget at the position (x, y) in the widget’s coordinate system. If there is no visible child widget at the specified position, the function returns 0.

と記述されてたので探してるもの遠かったので候補として外した。

QtWidgets.(QWidget|QLayout)クラスを探しても特に目ぼしいものはなかったので2つのクラスに共通として使われているQtCore.QObjectを焦点を当てていこう。

そのクラスの公式リファレンスを確認してみるとfindChild(), findChildren()関数が該当しそうであった。

使ってみる

findChild()関数を使う

まず最初はfindChild()関数を使ってみることとする。
第一引数にはPyTypeObjectが指定されているのでQPushButton入れる。第二引数は探すObjectの名前*1なので空欄にしておく。
とりあえず親ウィジェットから試してみる。

wdt = MainWidget()
c = wdt.findChild(QPushButton)
print(f"widget: {c}, text: {c.text()}")

として実行してみるとコンソールには

widget: <PySide2.QtWidgets.QPushButton object at 0x000000FDF8A70688>, text: button1

と表示された。
公式リファレンスには返り値がPyObjectと書いてあるので, findChild()関数はObjectの名前がついたものを探すのに便利そうな感じ。

findChildren()関数を使う

findChild()関数と同じ要領でやるが, 返り値がPySequenceなのでコードを以下のように変更する。

wdt = MainWidget()
sequence = wdt.findChildren(QPushButton)
print(sequence) 
for c in sequence:
    print(f"widget: {c}, text: {c.text()}")

すると, コンソールには

[<PySide2.QtWidgets.QPushButton object at 0x0000005ACD691848>, <PySide2.QtWidgets.QPushButton object at 0x0000005ACD691888>, <PySide2.QtWidgets.QPushButton object at 0x0000005ACD691788>, <PySide2.QtWidgets.QPushButton object at 0x0000005ACD6916C8>]
widget: <PySide2.QtWidgets.QPushButton object at 0x0000005ACD691848>, text: button3
widget: <PySide2.QtWidgets.QPushButton object at 0x0000005ACD691888>, text: button4
widget: <PySide2.QtWidgets.QPushButton object at 0x0000005ACD691788>, text: button2
widget: <PySide2.QtWidgets.QPushButton object at 0x0000005ACD6916C8>, text: button1

と出た。
ここで注目してほしいのは親ウィジェットに対するボタンウィジェットの数である。
試しに親ウィジェットに対する子ウィジェットを取得するためにchildren()関数を使用してみると

[<__main__.ChildWidget object at 0x0000009EE28C0648>, <PySide2.QtWidgets.QVBoxLayout object at 0x0000009EE28C0988>, <PySide2.QtWidgets.QPushButton object at 0x0000009EE28C0608>]

となり, ウィジェットに登録されてるボタンウィジェットに対してfindChildren()関数で表示されるボタンウィジェットの数が一致していないことがわかる。
これはfindChildren()関数が子ウィジェットウィジェット, つまり, 孫ウィジェットまで探すようになっているのである。
一見, 何の問題もなさそうだがもし, 親ウィジェットと子ウィジェットに同じ内容のウィジェットが存在したり, 大量のウィジェットが存在したりする場合はどうだろうか。
前者は親子関係を調べたりsetObjectName()とかでObjectNameをつけて判別すればいいのだが, 後者の場合はどうにもならない。
PySide2のもととなるQtのfindChildren()にはoptionsを追加できる*2が, PySide2にはそれが存在しない*3
そこで, findChildren()を使わずにウィジェットを探すことにした。

その1のまとめ

  • ウィジェットを探すにはQtCore.QObject.(findChild()|findChildren())がつかえそう。
  • 使う場合, 孫ウィジェットも含むウィジェットも候補となる。
  • 用途としては, ObjectNameを設定しそれを探し出すことに使える。

その2: レイアウトからウィジェットを探す

ウィジェットを取得する

その1ではウィジェットから子ウィジェットを探したがその2ではレイアウトから子ウィジェットを取得してみようと思う。

レイアウトを取得する

まず親ウィジェットのレイアウトを取得するためにlayout()関数を使う。
次がなかなかややこしい。
レイアウトに登録されているウィジェットを探すためにchildren()関数を使ったのだが, 返ってきた値が

[]

となってしまった。

これで数日間悩んだのだが, 調べてみるとどうやらレイアウトの基礎となるQLayoutにウィジェットを追加する場合, "Widget"として追加されるのではなく, "WidgetItem"として追加されるのでchildren()では空のリストが返ってくるそう。
そのため, レイアウトにあるウィジェットを調べたいときはitemAt()関数を使い, ウィジェットの詳細を知るにはlayout().widget()とすればいい。
なお, itemAt()関数の引数はint, 返り値はQWidgetItemなので使用する際はcount()関数を使ってウィジェットアイテムの数を取得してforループで回してそれぞれのウィジェットを取得することとなる。

目的のウィジェットか判別する

forループを回して返ってきたウィジェットが目的のウィジェットか判別する。目的ではないウィジェットtext()関数を使用してもエラーの原因になりかねない。
ではどのようにして判別すればいいのか。
ここで, ウィジェットをどのように実装したのかを思い出してほしい。ウィジェットを作成する際, 一番最初にすることはなにか。
それはインスタンスである。
つまり, 返ってきたウィジェットが目的のウィジェットインスタンスであるかを確認すればいいので,

isinstance(<ウィジェット>, <目的のウィジェット>)

isinstance()関数を使えば良い。

最終的なコード

"button3"を押したら"button2"のテキストを変更する。

class GrandsonWidget(QWidget):
    def __init__(self, *args, **kwargs):
        super().__init__(*args, **kwargs)

        btn3 = QPushButton("button3")
        btn3.clicked.connect(self.on_clicked)
        btn4 = QPushButton("button4")
        grandson_layout = QVBoxLayout()

        grandson_layout.addWidget(btn3)
        grandson_layout.addWidget(btn4)

        self.setLayout(grandson_layout)

    def on_clicked(self):
        for i in range(self.parent().layout().count()):
            c = self.parent().layout().itemAt(i).widget()
            if isinstance(c, QPushButton):
                if c.text() == "button2":
                    c.setText("clicked!")

実行結果:
f:id:KashEight:20180730154500g:plain
これで最初の目的を達成することができた。

余談

その1, その2を見て「普通にchildren()使ってisinstance()で判別すればいいのでは。」と思う人はいるのではないか。
自分もこの記事を書いてる途中に気づいた, まあその場その場で使い分ければいいと思う。

参考にしたサイト

python - Get a layout's widgets in PyQT - Stack Overflow

脚注

kivyでシステムフォントを使用できるようにする

デフォルトでシステムフォントを使用したい場合

from kivy.resources import resource_add_path
from kivy.core.text import LabelBase, DEFAULT_FONT

resource_add_path("<システムフォントのパス>")
LabelBase.register(DEFAULT_FONT, "<使用したいフォントのファイル名>")

適宜システムフォントを使用したい場合

今回はLabelのみで行う。

kvlangなし:

from kivy.app import App
from kivy.uix.label import Label

class MainApp(App):
    def build(self):
        return Label(text="hoge", font_name="<使用したいフォントのファイル名>")

kvlangあり:
(main.py)

from kivy.app import App

class MainApp(App):
    pass

(main.kv)

Label
    text: "hoge"
    font_name: "<使用したいフォントのファイル名>"

KivyでPythonのみ使用して色んなものを実装する

Kv LanguageをPython側でどう処理されてるか見るためにPythonのみで色々実装しようとしたら失敗続きで2日程悩んだので自戒をこめて書いとく。

Button実装

main.py:

from kivy.app import App
from kivy.uix.button import Button


class MainApp(App)
    def build(self):
        pass

main.kv:

Button:
    text: "hoge"

from kivy.app import App


class MainApp(App):
    def build(self):
        return Button(text="hoge")

と書き換えられる。

Labelも同じ感じなので割愛

Buttonを押したときのEvent処理

Python Kivyの使い方① ~Kv Languageの基本~の10項を例にする。
今回はクラスを新たに作らず全てMainAppクラス内で済ませるようにした。(正直、新しくクラス作ったほうが可読性は上がる)

main.py:

from kivy.app import App
from kivy.properties import StringProperty


class MainApp(App):
    text = StringProperty()
    btn_text = StringProperty()

    def __init__(self, **kwargs):
        super().__init__(**kwargs)
        self.text = ""
        self.btn_text = "Button"

    def build(self):
        pass

    def pressed(self):
        if self.text != "":
            self.text = ""
            self.btn_text = "Blank"
        else:
            self.text = "Hello World"
            self.btn_text = "Changed"

main.kv:

BoxLayout:
    orientation: 'vertical'
    size: root.size

    Label:
        id: label1
        font_size: 68
        text: app.text

    Button:
        id: button1
        text: app.btn_text
        font_size: 68
        on_press: app.pressed()

from kivy.app import App
from kivy.uix.boxlayout import BoxLayout
from kivy.uix.label import Label
from kivy.uix.button import Button

class MainApp(App):
    def __init__(self, **kwargs):
        super().__init__(**kwargs)
        self.label = Label(text="", font_size=48)
        self.button = Button(text="Button", font_size=48, on_press=self.pressed)

    def pressed(self, *args):
        if self.label.text != "":
            self.label.text = ""
            self.button.text = "Blank"
        else:
            self.label.text = "Hello World"
            self.button.text = "Changed"

    def build(self):
        boxlay = BoxLayout(orientation="vertical")
        boxlay.add_widget(self.label)
        boxlay.add_widget(self.button)
        return boxlay

と書き換えられる。

ここで注意が2つある。

一つは

self.button = Button(text="Button", font_size=48, on_press=self.pressed)

on_press=self.pressed()とはしないこと。
この場合、Event: on_pressの値をcallback関数に指定する必要があり、self.pressed()だと返された(returnで返ってきた)値を代入してしまうこととなる。

もう一つは

def pressed(self, *args):

で必ず引数を(使わずとも)追加する必要がある。理由としては、pressed関数はcallback関数なので引数に自身のインスタンス(self)ともう一つ、呼んだ関数のインスタンス(この場合MainApp())を必要とする。そのため、pressed関数を利用するためにはこの場合2つの引数が必要となる。 呼んだ関数のインスタンスを利用したい場合は*argsinstanceとかに変えれば良い。使わないなら基本的に引数は*argsで十分。

参考:
公式リファレンス - Button (英語)

また、pressed関数に他の引数を追加したい場合はfunctoolsモジュールからpartialをimportして

self.button = Button(text="Button", font_size=48, on_press=partial(self.pressed, <arg1>, <arg2>...)

とする。(self.pressed(<arg1>, <arg2>...)は先述したとおりreturnされた値を代入してしまう。)
そしてpressed関数を

def pressed(self, instance, <arg1>, <arg2>, ...):

とする。

LabelまたはButtonのtextを変えたい場合は

self.label = Label(text="", font_size=48)
self.button = Button(text="Button", font_size=48, on_press=self.pressed)

のように変数を作り、

self.label.text = ""
self.button.text = "Blank"

のようにクラス変数の値を変えれば良い。

今回示した例とけっこう違う方法で実装することも可能なので、随時更新していきたい。