takuroooのブログ

勉強したこととか

GoのArrayとSliceの違い

先週からGoを勉強し始めた。
blog.golang.org
ArrayとSliceがいまいちわからなかったが、この記事を読んでArrayとSliceの違いを学んだので自分なりにまとめてみる。

目次

ArrayとSliceの違い(記事を読む前の認識)

  • Array
    • 固定長の配列
    • C言語の配列と同じように変数は配列の先頭アドレスを保持する。
  • Slice
    • 可変長の配列。

と思っていたけど調べてみると間違っている部分があった。

以下ArrayとSliceが何者なのかをまとめていく。

Array

Array型とはなにか

Array型は配列全体を示すデータ型とのこと。これは配列の長さまでを含めて型として定義しているということ。*1

なので同じArray型でも長さが異なれば違う型として判定される。 つまり、以下のコードはエラーになる。

package main

import (
    "fmt"
)

func doSomething(arr [4]int) {
    fmt.Println(arr)
}

func main() {
    arr := [3]int{1, 2, 3}

    // [3]int と [4]intは型が違うためエラーになる
    doSomething(arr) 
}

arrの型は、int型の値を持つ配列ではなく「int型の値を持つ長さ3の配列」という意味になる。関数の引数の型は「int型の値を持つ長さ4の配列」なので型が一致せずエラーになる。

Array型の変数は何を意味しているのか

Array型の変数は、C言語の配列のように先頭のアドレスを保持しているわけではない。 固定長の配列という観点ではC言語の配列と同じだが、この点が違う。

以下のコードではArray型の変数の値をアドレス表記としてプリントしようとしているが、正しくプリントされない。
Array型の変数はC言語の配列のように配列の先頭アドレスを保持しているわけではないので、変数をアドレスとしてプリントしようとすると意図しない表示になる。

package main

import (
    "fmt"
)

func main() {
    arr := [3]int{1, 2, 3}
    // アドレスが表示されるのが期待だけど...
    fmt.Printf("%p\n", arr) // %!p([3]int=[1 2 3])
}

一方、Sliceは同じやり方でアドレスが正しく表示される。詳細は後述する。

Array型の変数は値渡し

Array型の変数は先頭のアドレスを保持した変数ではないため関数に私たりや変数の代入すると実態のコピーが発生する。

package main

import (
    "fmt"
)

func main() {
    arr1 := [3]int{1, 2, 3}
    arr2 := arr1 // arr2のために新しいメモリを確保してarr1の値がコピーされる
    arr2[0] = 0

    // arr1とarr2が同じ配列を共有していないためarr2を変更してもarr1に影響がない
    fmt.Println(arr1, arr2) // [1 2 3] [0 2 3]
}

関数の引数としてArrayを渡した場合もコピーが渡される。

package main

import (
    "fmt"
)

func doSomething(arr [3]int) {
    arr[0] = 0 // Arrayのコピーの値を変えているためarr1には影響がない
}

func main() {
    arr1 := [3]int{1, 2, 3}
    doSomething(arr1)
    fmt.Println(arr1) //[1 2 3]
}

もちろんArrayのアドレスを引数として渡せば、Arrayのコピーは発生せずにdoSomethingの中で行なった処理はarr1に影響するようになる。

Slice

Slice型とはなにか

Slice型は配列を指すデータ型である。どの型のArrayを指すかだけを定義している。なのでSlice型の宣言には配列の長さを含めない。

package main

import (
    "fmt"
)

func main() {
    slice := []int{1, 2, 3}
    arr := [3]int{1, 2, 3}
    fmt.Printf("slice %T\n", slice) # slice []int
    fmt.Printf("arr   %T\n", arr)   # arr   [3]int
}

%Tで型を表示してみるとSlice型は長さの情報が表示されないが、Array型は長さが表示されているのがわかる。

Slice型の変数は何を意味しているのか

Slice型の変数はArrayの先頭アドレスを保持している。これはC言語の配列を保持する変数と似ている。
Arrayにもポインタがあるがこれは固定長の配列を指すアドレスである。サイズが異なる配列を指すことはできない。Slice型の変数はArrayの長さに制限されない。

package main

import (
    "fmt"
)

func main() {
    slice := []int{1, 2, 3}
    fmt.Printf("%p\n", slice) // 16進数でアドレスが表示される
}

Slice型の変数の値を%pで表示するとArray型のときとは異なりエラーしないでアドレスが表示される。
slice := []int{1, 2, 3}この宣言ではsliceにArrayの先頭アドレスが保持される。このとき背後で長さが3の配列が生成されている。

下記コードではArrayからSliceを生成しているが、Sliceが保持しているアドレスがArrayのアドレスを保持していることがわかる。同じアドレスを見ているので、arrもしくはsliceの変更はお互いに影響しあう。

package main

import (
    "fmt"
)

func main() {
    arr := [3]int{1, 2, 3} // Array型
    var slice []int // Slice型

    slice = arr[:] // arr[:]はArrayからSliceを生成する

    // 同じアドレスが表示される
    fmt.Printf("%p %v\n", slice, slice) // 0xc0000141a0 [1 2 3]
    fmt.Printf("%p %v\n", &arr, arr) // 0xc0000141a0 [1 2 3]

    arr[0] = 0

    // 同じアドレスを指しているのでarrの変更はsliceにも反映される
    fmt.Printf("%p %v\n", slice, slice) // 0xc0000141a0 [0 2 3]
    fmt.Printf("%p %v\n", &arr, arr) // 0xc0000141a0 [0 2 3]
}

Slice型の変数は参照渡し

Slice型はArray型の先頭アドレスを保持している。これを関数の引数や他の変数に代入するということは単純にアドレスを渡しているだけなので、アドレスを渡された側も同じアドレスをみることになる。

package main

import (
    "fmt"
)

func doSomething(slice []int) {
    slice[0] = 0
    fmt.Printf("%p %d\n", slice, slice) // 0xc0000141a0 [0 2 3]

}
func main() {
    slice := []int{1, 2, 3}
    doSomething(slice)
    fmt.Printf("%p %d\n", slice, slice) // 0xc0000141a0 [0 2 3]
}

*1:GoのブログではArrayはインデックス付きのフィールドを持った構造体としても考えられると書いてある。

Qt for Python Exif Viewer

QTreeWidgetとQTextEditとQSplitterとQStackedWidgetを使ってExifViewerを作った。

f:id:takuroooooo:20200729223518p:plain

github.com

以前作ったViewerはQTreeWidgetメインでGUIを作成したが、今回はQSplitterとQStackedWidgetを使ってページが切り替わるようなUIにした。

自作ExifParser

UI以外の変更点として、前回はPILの_getexif()を使ってExifを取得していたが、今回はExifのParserを自作した。

ExifViewer/exif_reader.py at master · takurooo/ExifViewer · GitHub

下記のように書くとExifのtagの名前と値をプリントすることができる。

from common.exif_reader import ExifReader
exif_reader = ExifReader(path_to_your_jpegfile) # Parse exif tags
ifds =exif_reader.get_exif()
for ifd_name, ifd in ifds.items():
    for tag_name, tag in ifd.items():
        print(tag_name, tag.val)

またテキストにExif情報を出力することもできる。

from common.exif_reader import ExifReader
exif_reader = ExifReader(path_to_your_jpegfile) # Parse exif tags
exif_reader.save_log(text_file_path)

このテキスト保存機能はExifViewerのメニューバーにあるSaveボタンを押すと実行するようにしてある。

PySide2 メニューバーとアイコン

Qtではメニューアイコンがデフォルトで用意されている。
メニューバーとアイコンの設定は_make_toolbar()で実行している。

ExifViewer/exif_viewer.py at master · takurooo/ExifViewer · GitHub

    def _make_toolbar(self):
        save_icon = QApplication.style().standardIcon(QStyle.SP_DialogSaveButton)
        save_action = QAction(icon=save_icon, text='save', parent=self)
        save_action.triggered.connect(self._save_text)

        exit_icon = QApplication.style().standardIcon(QStyle.SP_DialogCloseButton)
        exit_action = QAction(icon=exit_icon, text='exit', parent=self)
        exit_action.triggered.connect(self._quit)

        # ツールバー作成
        self.toolbar = self.addToolBar('tool bar')
        self.toolbar.setIconSize(QSize(35, 35))
        self.toolbar.setFixedHeight(35)
        self.toolbar.addAction(save_action)
        self.toolbar.addAction(exit_action)

save_action.triggered.connect(self._save_text)でボタンが押された時に実行される関数を設定している。
QtではSignalとSlotという考え方がある。ユーザーがボタンを押したりするとSignalが発行され、Signalに紐づいたSlot(関数)が実行される。
save_action.triggered.connect(self._save_text)はSignalとSlotの紐付けを行なっている。

Qt for Python Signals and Slots - Qt Wiki

QStackedWidgetの基本的なこと

Qt for Python(PySide2)のQStackedWidgetについて勉強したのでまとめていく。

目次

QStackedWidgetとは

QStackedWidgetを使うとWidgetを重ねることができる。

f:id:takuroooooo:20200723132848p:plain

QStackedWidgetでまとめられたWidgetは常に1つしか表示されない。そのためQStackedWidgetを使用すると、ある時はWidget1を表示して、ある操作をされたらWidget2に切り替えるみたいな制御が可能となる。

これにより状況に応じて必要なWidgetだけを表示でき、UIの表示スペースを小さくすることができる。

StackedWidgetを表示する

f:id:takuroooooo:20200723133647p:plain

import sys
from PySide2.QtWidgets import QApplication, QTextEdit, QStackedWidget

app = QApplication(sys.argv)

qw_text_edit_1 = QTextEdit()
qw_text_edit_1.append('1')

qw_text_edit_2 = QTextEdit()
qw_text_edit_2.append('2')

qw_stack = QStackedWidget()
 # QStackedWidgetにTextEditを2つ追加する
qw_stack.addWidget(qw_text_edit_1)
qw_stack.addWidget(qw_text_edit_2)
print(qw_stack.currentIndex())

qw_stack.show() # 最初に追加したTextEditが表示される

sys.exit(app.exec_())
  • addWidgetを使ってQTextEditを2つQStackedWidgetに追加している。
  • この場合最初に追加したQTextEditが表示される。

StackedWidgetに登録されたwidgetをインデックスで指定して表示する

f:id:takuroooooo:20200723133702p:plain

import sys
from PySide2.QtWidgets import QApplication, QTextEdit, QStackedWidget

app = QApplication(sys.argv)

qw_text_edit_1 = QTextEdit()
qw_text_edit_1.append('1')

qw_text_edit_2 = QTextEdit()
qw_text_edit_2.append('2')

qw_stack = QStackedWidget()
idx_qw_text_edit_1 = qw_stack.addWidget(qw_text_edit_1)
idx_qw_text_edit_2 = qw_stack.addWidget(qw_text_edit_2)
print(idx_qw_text_edit_1, idx_qw_text_edit_2)

 # 表示するwidgetをインデックスで指定する
qw_stack.setCurrentIndex(idx_qw_text_edit_2)
print(qw_stack.currentIndex())

qw_stack.show()

sys.exit(app.exec_())
  • setCurrentIndexを使って表示するwidgetをインデックスで指定する。
  • インデックスはaddWidgetの戻り値で取得できる。
  • 現在設定されているインデックスはcurrentIndexを使って取得できる。

StackedWidgetに登録されたwidgetwidgetインスタンスで指定して表示する

f:id:takuroooooo:20200723133702p:plain

import sys
from PySide2.QtWidgets import QApplication, QTextEdit, QStackedWidget

app = QApplication(sys.argv)

qw_text_edit_1 = QTextEdit()
qw_text_edit_1.append('1')

qw_text_edit_2 = QTextEdit()
qw_text_edit_2.append('2')

qw_stack = QStackedWidget()
qw_stack.addWidget(qw_text_edit_1)
qw_stack.addWidget(qw_text_edit_2)

 # 表示するwidgetをwidgetのインスタンスで指定する
qw_stack.setCurrentWidget(qw_text_edit_2)
print(qw_stack.currentWidget())

qw_stack.show()

sys.exit(app.exec_())

TreeをクリックしてTextEditを切り替える

f:id:takuroooooo:20200723134408p:plainf:id:takuroooooo:20200723134411p:plainf:id:takuroooooo:20200723134414p:plain

import sys
from PySide2.QtWidgets import QApplication, QTreeWidget, QTreeWidgetItem, QTextEdit, QStackedWidget, QSplitter

app = QApplication(sys.argv)

qw_stack = QStackedWidget()

qw_tree = QTreeWidget()
qw_tree.setHeaderLabels(["test"])


def tree_item_clicked(item, column):
    print(item, column, item.type())
    # TreeがクリックされたときにTextEditを切り替える
    qw_stack.setCurrentIndex(item.type())


for i in range(3):
    text = 'page No.' + str(i+1)
    qw_tree_item = QTreeWidgetItem([text], type=i)
    qw_tree.addTopLevelItem(qw_tree_item)

    qw_text_edit = QTextEdit()
    qw_text_edit.append(text)

    stack_idx = qw_stack.addWidget(qw_text_edit)
    # print(stack_idx)

# Treeがクリックされたときに呼ばれる関数を登録
qw_tree.itemClicked.connect(tree_item_clicked)

qw_splitter = QSplitter()
qw_splitter.addWidget(qw_tree)
qw_splitter.addWidget(qw_stack)

qw_splitter.show()

sys.exit(app.exec_())

ソースコード

github.com

QSplitterの基本的なこと

Qt for Python(PySide2)のQSplitterについて勉強したのでまとめていく。
QSplitterを使うと複数のwidgetの間にサイズを調整できるスプリッターを置くことができる。

目次

SplitterでTextEditを水平に2つ表示する

f:id:takuroooooo:20200723113219p:plain

import sys
from PySide2.QtWidgets import QApplication, QSplitter, QTextEdit
from PySide2.QtCore import Qt

app = QApplication(sys.argv)

qw_text_edit_left = QTextEdit()
qw_text_edit_left.append('left')

qw_text_edit_right = QTextEdit()
qw_text_edit_right.append('right')

qw_splitter = QSplitter()
qw_splitter.addWidget(qw_text_edit_left) # Splitterにwidgetを追加
qw_splitter.addWidget(qw_text_edit_right) # Splitterにwidgetを追加

qw_splitter.show()

sys.exit(app.exec_())
  • addWidgetを使うとwidgetをQSplitterに追加できる。
  • QSplitterは何も設定しないと水平のスプリッターを生成する。
  • QSplitterに追加されるwidget(この例ではQTextEdit)はshow()を呼ぶ必要はない。

SplitterでTextEditを垂直に2つ表示する

f:id:takuroooooo:20200723113231p:plain

import sys
from PySide2.QtWidgets import QApplication, QSplitter, QTextEdit
from PySide2.QtCore import Qt

app = QApplication(sys.argv)

qw_text_edit_top = QTextEdit()
qw_text_edit_top.append('top')

qw_text_edit_bottom = QTextEdit()
qw_text_edit_bottom.append('bottom')

qw_splitter = QSplitter()
qw_splitter.setOrientation(Qt.Orientation.Vertical)  # Splitterを垂直に設定
print(qw_splitter.orientation())
qw_splitter.addWidget(qw_text_edit_top)
qw_splitter.addWidget(qw_text_edit_bottom)

qw_splitter.show()

sys.exit(app.exec_())
  • setOrientationで水平に分割するか垂直に分割するかを選択できる。

Splitterの左右の比率を変える

f:id:takuroooooo:20200723113246p:plain

import sys
from PySide2.QtWidgets import QApplication, QSplitter, QTextEdit

app = QApplication(sys.argv)

qw_text_edit_left = QTextEdit()
qw_text_edit_left.append('left')

qw_text_edit_right = QTextEdit()
qw_text_edit_right.append('right')

qw_splitter = QSplitter() # Orientationの初期値は水平
qw_splitter.addWidget(qw_text_edit_left)
qw_splitter.addWidget(qw_text_edit_right)

qw_splitter_size = qw_splitter.size() # Splitterのサイズを取得する
qw_splitter_size_width = qw_splitter_size.width() # Splitterの横サイズを取得する
qw_splitter.setSizes([qw_splitter_size_width*0.1, qw_splitter_size_width*0.9]) # Splitterの横の比率を1:9に変更する
print(qw_splitter.size()) # Splitter全体のサイズ
print(qw_splitter.sizes()) # Splitterの子widgetごとのサイズ

qw_splitter.show()

sys.exit(app.exec_())
  • setSizesでリストを渡すとQSplitterに登録されている各widgetのサイズを設定できる。
  • この例ではQSplitterに登録されているwidgetが2つなので、要素が2つあるリストを設定している。

Splitterの子widgetのサイズ調整をしたときに調整完了後にサイズを変更する

f:id:takuroooooo:20200723114248g:plain

import sys
from PySide2.QtWidgets import QApplication, QSplitter, QTextEdit

app = QApplication(sys.argv)

qw_text_edit_left = QTextEdit()
qw_text_edit_left.append('left')

qw_text_edit_right = QTextEdit()
qw_text_edit_right.append('right')

qw_splitter = QSplitter()
qw_splitter.addWidget(qw_text_edit_left)
qw_splitter.addWidget(qw_text_edit_right)

qw_splitter.setOpaqueResize(False) # 子widgetのサイズ調整を操作完了後に行う
print(qw_splitter.opaqueResize())

qw_splitter.show()

sys.exit(app.exec_())
  • setOpaqueResizeでTrueを設定するとスプリッターを操作したときに即時に変更が反映される。
  • setOpaqueResizeでFalseを設定すると上記Gifのようにスプリッターを操作した後にスプリッターが実際に移動する。

Splitterにwidgetをインデックスで指定した位置に追加する

f:id:takuroooooo:20200723113313p:plain

import sys
from PySide2.QtWidgets import QApplication, QSplitter, QTextEdit

app = QApplication(sys.argv)

qw_text_edit_left = QTextEdit()
qw_text_edit_left.append('left')

qw_text_edit_right = QTextEdit()
qw_text_edit_right.append('right')

qw_splitter = QSplitter()
qw_splitter.addWidget(qw_text_edit_left)
qw_splitter.addWidget(qw_text_edit_right)

qw_text_edit_center = QTextEdit()
qw_text_edit_center.append('center')
qw_splitter.insertWidget(1, qw_text_edit_center) # index 1 の位置にwidgetを追加

print(qw_splitter.indexOf(qw_text_edit_left))
print(qw_splitter.indexOf(qw_text_edit_center))
print(qw_splitter.indexOf(qw_text_edit_right))

qw_splitter.show()

sys.exit(app.exec_())
  • insertWidgetを使うとインデックスで指定した位置にwidgetを追加することができる。

ソースコード

github.com


QTextEditの基本的なこと

前回に引き続きQt for Python(PySide2)のQTextEditについて勉強したのでまとめていく。

QTextEditを使うとテキストを入力できるフォームを作成することができる。

目次

TextEditを表示する

f:id:takuroooooo:20200126102928p:plain

import sys
from PySide2.QtWidgets import QApplication, QTextEdit

app = QApplication(sys.argv)

qw_text_edit = QTextEdit()
qw_text_edit.show()

sys.exit(app.exec_())

ユーザー自身が文字を入力することができる。 f:id:takuroooooo:20200126102947p:plain

TextEditに文字をを表示する

f:id:takuroooooo:20200126172845p:plain

import sys
from PySide2.QtWidgets import QApplication, QTextEdit

app = QApplication(sys.argv)

qw_text_edit = QTextEdit()
qw_text_edit.append('This is a text edit widget.') # 文字を表示する
qw_text_edit.show()

sys.exit(app.exec_())

appendを使うことで最初に表示するテキストを設定できる。

編集禁止にする

f:id:takuroooooo:20200126173748p:plain

import sys
from PySide2.QtWidgets import QApplication, QTextEdit

app = QApplication(sys.argv)

qw_text_edit = QTextEdit()
qw_text_edit.setReadOnly(True)  # 編集禁止にする
qw_text_edit.append('This is a text edit widget.')
qw_text_edit.show()

sys.exit(app.exec_())

文字の色を変える

f:id:takuroooooo:20200126173312p:plain

import sys
from PySide2.QtWidgets import QApplication, QTextEdit
from PySide2.QtGui import QColor

app = QApplication(sys.argv)

qw_text_edit = QTextEdit()
qw_text_edit.setTextColor(QColor(255, 0, 0, 255)) # 文字の色を赤色にする
qw_text_edit.append('This is a text edit widget.')
qw_text_edit.show()

sys.exit(app.exec_())

文字の大きさを変える

f:id:takuroooooo:20200126173515p:plain

import sys
from PySide2.QtWidgets import QApplication, QTextEdit

app = QApplication(sys.argv)

qw_text_edit = QTextEdit()
qw_text_edit.setFontPointSize(30)  # 文字の大きさを変える
qw_text_edit.append('This is a text edit widget.')
qw_text_edit.show()

sys.exit(app.exec_())

フォントを設定する

f:id:takuroooooo:20200126175240p:plain

import sys
from PySide2.QtWidgets import QApplication, QTextEdit
from PySide2.QtGui import QFont

app = QApplication(sys.argv)

qw_text_edit = QTextEdit()
qt_font = QFont("Monaco")  # monaco fontを生成
qt_font.setStyleHint(QFont.Helvetica)  # monacoが使用できない場合はHelveticaにする
qw_text_edit.setFont(qt_font)  # fontを設定
qw_text_edit.append('This is a text edit widget.')
qw_text_edit.show()

sys.exit(app.exec_())

もしMonacoがなければsetStyleHintで設定されたフォントが適用される。

カーソルの位置を先頭行に設定する

f:id:takuroooooo:20200126184910p:plain

import sys
from PySide2.QtWidgets import QApplication, QTextEdit
from PySide2.QtGui import QTextCursor

app = QApplication(sys.argv)

qw_text_edit = QTextEdit()
for i in range(20):
    qw_text_edit.append(str(i))

first_row_block = qw_text_edit.document().findBlockByLineNumber(0)  # 1行目のblockを取得
text_cursor = QTextCursor(first_row_block)  # カーソルを取得
qw_text_edit.setTextCursor(text_cursor)  # cursorを1行目に設定

qw_text_edit.show()

sys.exit(app.exec_())

カーソルに関する設定をしないと最後にappendした行にカーソルがあった状態で起動する。

ソースコード

github.com

QTreeWidgetを使ってExifViewerを作る

前回紹介したQt for PythonのQTreeWidgetを使って簡単なExifViewerを作ってみる。
コード全体(60行くらい)は最後に記載。

takuroooooo.hatenablog.com

目次

全体構成

構成は下記図のような感じ。
入力はJPEGファイルのみ対応。

f:id:takuroooooo:20200113230959p:plain

ExifViewer完成画面

今回作るExifViewerの画面。
GUIはQt for Pythonで作る。

f:id:takuroooooo:20200113224124p:plain

f:id:takuroooooo:20200113224003p:plain

f:id:takuroooooo:20200113223957p:plain

以下コードの簡単な説明をしていく。

処理手順

1.JPEGからExifを取得する

まずPILを使ってJPEGの中にあるExif情報を取り出す。
PILはpip install pillowでインストールできる。

Exif情報は_getexif()を使うとExif情報が辞書形式で取得できる。

from PIL import Image
exif = Image.open(img_path)._getexif()

exif変数は辞書になっていて、{tag_id: value}の形式になっている。exifはどんな情報なのかを識別するためのtag_idがあらかじめ用意されている。tag_idは16バイトの数字で定義されている。

www.vieas.com

viewerで表示するときはtag_idだと何の情報なのかよくわからないので、人間が分かるtag_nameに変換する。
これはfrom PIL.ExifTags import TAGS, GPSTAGSを使うと簡単に変換できる。

下記コードはtag_idとtag_nameとtagの値を出力するコード。

from PIL.ExifTags import TAGS, GPSTAGS
for tag_id, value in exif.items():
    tag_name = TAGS[tag_id]
    print(tag_id, tag_name, value)

これを実行すると以下のような出力が得られる。

36864 ExifVersion b'0220'
37121 ComponentsConfiguration b'\x01\x02\x03\x00'
40960 FlashPixVersion b'0100'
36867 DateTimeOriginal 2008:10:22 16:28:39
36868 DateTimeDigitized 2008:10:22 16:28:39
・
・
34853 GPSInfo {1: 'N', 2: ((43, 1), (28, 1), ....
・
・

GPSTAGSExif情報の中にあるGPS情報に対して使う。GPS情報はtag_nameがGPSInfoの中にある。GPSInfo{tag_id: value}という形式の値を持っているので、同じようにGPSTAGS[tag_id]とすればtag_nameが取り出せる。

今回はExif情報とGPS情報を親ツリーとして表示したいので、まずはExif情報からGPS情報だけを取り出す関数をつくる。こうしておくことで後々のコードをわかりやすく書ける。

def extract_gps_info(exif):
    for exif_id, exif_value in exif.items():
        exif_tag = TAGS.get(exif_id, str(exif_id))
        if exif_tag == 'GPSInfo':
            del exif[exif_id] # Exif情報からGPS情報を削除する.
            return exif_value # GPS情報をリターンする.
    return None

exif = Image.open(img_path)._getexif()
gps = extract_gps_info(exif)

2.Exif情報を使ってTreeを生成する

{tag_id: value}形式のExif情報とGPS情報が取得できたので、次にこの情報を使ってTreeを作成する。
Treeの生成はQTreeWidget()QTreeWidgetItemを使う。 今回はcreate_top_level_itemという関数を作って、Exif情報のツリーとGPS情報のツリーを生成する。
この関数でリターンしたQTreeWidgetItemQTreeWidget.addTopLevelItem()の引数にすることでTreeに登録している。

def create_top_level_item(top_name, id_val, id_to_name):
    top_level_item = QTreeWidgetItem([top_name])  # 親ツリーアイテム生成

    for tag_id, val in id_val.items():
        tag_name = id_to_name.get(tag_id, str(tag_id))
        child_item = QTreeWidgetItem([tag_name, str(val)])  # 子ツリーアイテム生成
        top_level_item.addChild(child_item)  # 親に子を追加

    return top_level_item

qw_tree = QTreeWidget()
qw_tree.resize(500, 500)
qw_tree.setAlternatingRowColors(True)
qw_tree.setHeaderLabels(["name", "val"])

# Exif情報のツリーをつくる.
top_level_item = create_top_level_item('exif', exif, TAGS)
qw_tree.addTopLevelItem(top_level_item)

# GPS情報のツリーをつくる.
if gps is not None:
    top_level_item = create_top_level_item('gps', gps, GPSTAGS)
    qw_tree.addTopLevelItem(top_level_item)

全体コード

import sys
from PySide2.QtWidgets import QApplication, QTreeWidget, QTreeWidgetItem
from PIL import Image
from PIL.ExifTags import TAGS, GPSTAGS


def extract_gps_info(exif):
    for exif_id, exif_value in exif.items():
        exif_tag = TAGS.get(exif_id, str(exif_id))
        if exif_tag == 'GPSInfo':
            del exif[exif_id]  # Exif情報からGPS情報を削除する.
            return exif_value  # GPS情報をリターンする.
    return None


def create_top_level_item(top_name, id_val, id_to_name):
    top_level_item = QTreeWidgetItem([top_name])  # 親ツリーアイテム生成

    for tag_id, val in id_val.items():
        tag_name = id_to_name.get(tag_id, str(tag_id))
        child_item = QTreeWidgetItem([tag_name, str(val)])  # 子ツリーアイテム生成
        top_level_item.addChild(child_item)  # 親に子を追加

    return top_level_item


def main(img_path):
    app = QApplication([])

    exif = Image.open(img_path)._getexif()
    # for tag_id, value in exif.items():
    #     print(tag_id, TAGS[tag_id], value)
    gps = extract_gps_info(exif)

    qw_tree = QTreeWidget()
    qw_tree.resize(500, 500)
    qw_tree.setAlternatingRowColors(True)
    qw_tree.setHeaderLabels(["name", "val"])
    
    # Exif情報のツリーをつくる.
    top_level_item = create_top_level_item('exif', exif, TAGS)
    qw_tree.addTopLevelItem(top_level_item)

    # GPS情報のツリーをつくる.
    if gps is not None:
        top_level_item = create_top_level_item('gps', gps, GPSTAGS)
        qw_tree.addTopLevelItem(top_level_item)

    # qw_tree.expandAll()
    qw_tree.show()

    sys.exit(app.exec_())


if __name__ == "__main__":
    if len(sys.argv) != 2:
        sys.exit()
    img_path = sys.argv[1]
    main(img_path)

QTreeWidgetの基本的なこと

最近仕事で処理結果をGUIで表示させたいことがあったのでPythonのQtを勉強してみた。

PythonのQtにはPyQtとQt for Python(PySide2)があるけど、今回は最近登場したQt for Python(PySide2)を使って簡単なTree構造の作り方をまとめてみようと思う。

Qt for Pythonは以下のコマンドでインストールできる。

pip install pyside2

Qt for Python公式ページ www.qt.io

目次

TreeのベースとなるWindowを表示する

f:id:takuroooooo:20200112105606p:plain

import sys
from PySide2.QtWidgets import QApplication, QTreeWidget

app = QApplication(sys.argv)

qw_tree = QTreeWidget()
qw_tree.show()

sys.exit(app.exec_())

基本的なこと

  • QtではWidgetと呼ばれる部品を使うことで色々なGUIを作ることができる。
  • QApplication()Widget関連の初期化をするので、Widgetを使う前に実行しなければならない。
  • QTreeWidget()はツリー形式のGUIを作るためのWidget
  • Widgetを表示するためには、show()を実行する。show()は全てのWidgetが持つメソッド。
  • sys.exit(app.exec_())でQtのメインループに入る。

ということで、GUIを作るためにはQApplication()sys.exit(app.exec_())の間で

  1. 必要なWidgetを定義
  2. Widgetのメソッドであるshow()を実行

が必要になる。

TreeにHeaderをつける

f:id:takuroooooo:20200112114557p:plain

import sys
from PySide2.QtWidgets import QApplication, QTreeWidget

app = QApplication(sys.argv)

qw_tree = QTreeWidget()
qw_tree.setHeaderLabels(["name", "tel", "mail"])  # Headerをつける
qw_tree.show()

sys.exit(app.exec_())

QTreeWidget.setHeaderLabels(labels)

  • リスト形式の変数を渡すとヘッダを生成してくれる。
  • 同様にヘッダを生成することができるメソッドにQTreeWidget.setHeaderItem()QTreeWidget.setHeaderLabel()があるが、できることは一緒なのでsetHeaderLabels()を使っておけばOK。

TreeにItemを追加する

f:id:takuroooooo:20200112120235p:plain

import sys
from PySide2.QtWidgets import QApplication, QTreeWidget, QTreeWidgetItem

app = QApplication(sys.argv)

qw_tree = QTreeWidget()
qw_tree.setHeaderLabels(["name", "tel", "mail"])
qw_tree_parent_item = QTreeWidgetItem(['family'])  # Treeに追加するItemを生成
qw_tree.addTopLevelItem(qw_tree_parent_item)  # TreeにItemを追加する
qw_tree.show()

sys.exit(app.exec_())

QTreeWidgetItem(names)

  • list形式の変数を渡すとTreeに追加するItemを生成してくれる。
  • QTreeWidget.addTopLevelItem()に生成したItemを設定すると、TreeにItemが登録される。
  • QTreeWidget.addTopLevelItems()を使うと複数Itemをリスト形式で渡すことができる。

Itemに子Itemを追加する

f:id:takuroooooo:20200112121225p:plain

import sys
from PySide2.QtWidgets import QApplication, QTreeWidget, QTreeWidgetItem

app = QApplication(sys.argv)

qw_tree = QTreeWidget()
qw_tree.setHeaderLabels(["name", "tel", "mail"])
qw_tree_parent_item = QTreeWidgetItem(['family'])
qw_tree_parent_item.addChild(QTreeWidgetItem(['A', '111-111-111', 'aaa@gmail.com']))  # Itemに子Itemを追加
qw_tree.addTopLevelItem(qw_tree_parent_item)
qw_tree.expandAll()  # TreeのItemを全て開く
qw_tree.show()

sys.exit(app.exec_())

QTreeWidgetItem.addChild(Item)

  • QTreeWidgetItem.addChild(Item)を使うとItemに子Itemを追加できる。
  • QTreeWidget.expandAll()を使うとデフォルトでTreeが開いた状態になる。
  • QTreeWidget.expandAll()QTreeWidgetItem.addChild(Item)の後に呼ぶ。

Treeに複数のItemをつける

これまでやってきたことを繰り返すだけで簡単に階層構造のTree GUIが作れる。

f:id:takuroooooo:20200112122553p:plain

import sys
from PySide2.QtWidgets import QApplication, QTreeWidget, QTreeWidgetItem

app = QApplication(sys.argv)

qw_tree = QTreeWidget()
qw_tree.setHeaderLabels(["name", "tel", "mail"])

qw_tree_parent_item_1 = QTreeWidgetItem(['family'])
qw_tree_parent_item_1.addChild(QTreeWidgetItem(['A', '111-111-111', 'aaa@gmail.com']))
qw_tree_parent_item_1.addChild(QTreeWidgetItem(['B', '222-222-222', 'bbb@gmail.com']))

qw_tree_parent_item_2 = QTreeWidgetItem(['school'])
qw_tree_parent_item_2.addChild(QTreeWidgetItem(['C', '333-333-333', 'ccc@gmail.com']))
qw_tree_parent_item_2.addChild(QTreeWidgetItem(['D', '444-444-444', 'ddd@gmail.com']))

qw_tree.addTopLevelItem(qw_tree_parent_item_1)
qw_tree.addTopLevelItem(qw_tree_parent_item_2)
qw_tree.expandAll()
qw_tree.show()

sys.exit(app.exec_())

ソースコード

github.com