Angular始めました – リアクティブフォームを使ってみる。そして、stackblitz.comのプログラムを組み込んでみる

 今日も見に来てくださってありがとうございます。石川さんです。
先日に引き続き、Angularを勉強しています。今回はリアクティブフォームを使って、サンプルプログラムを作ってみました。

出来上がりイメージ

  今回は、たまたまニュートン-ラフソン法について質問があったので、Angularで実装してみました。こんな感じになります。ルートを求めたい値「k」を入力して、Calcボタンをクリックすると、漸化式を繰り返します。二乗した結果が「k」との誤差0.01未満になったら終了します。

ニュートン-ラフソン法の実行結果

 今回も、statsblitz.comを利用してみました。作業中に、デフォルトで用意されているコンポーネントの「hello.component.ts」を削除してみたところ、以下のエラーがでました。しばらく解決できなくて、何かの設定があるのかと、ウロウロしてしまったので、回避策をメモしておきます。

Error in src/app/hello.component.ts (1:1)
File '/~/src/app/hello.component.ngtypecheck.ts' not found.

 何のことはありません、おかしな状態になっているだけ、ということのようでした。画面のプロジェクトエクスプローラーの左下の「DEPENDENCIES」をポイントすると、くるりとなった矢印が登場するので、そちらをクリックしたところ、しばらく待って、エラーが消えました。

DEPENDENCIESの解決

ソースコード

ソースコードは以下のとおりです。

まずは、コンポーネントのスクリプトです。リアクティブフォームのポイントは「FormGroup」ですね。TypeScript側で「myGroup」として定義しています。また、今回はFormBuilderを使って「k」を定義しました。また、Validatorsを使って、正の数をチェックするようにしました。
onSubmit()呼び出し時、漸化式で計算を実行して、二乗した結果がkに近づいて差が0.01を下回ったところで処理を完了します。

import { Component, OnInit } from '@angular/core';
import { FormGroup, FormBuilder, Validators } from '@angular/forms';

@Component({
  selector: 'app-x2',
  templateUrl: './x2.component.html',
  styleUrls: ['./x2.component.css'],
})
export class X2Component implements OnInit {
  myGroup: FormGroup;
  results: string[];
  guess: number;

  constructor(private fb: FormBuilder) {}

  ngOnInit() {
    this.myGroup = this.fb.group({
      k: [24, Validators.min(0)],
    });
  }

  get k() { return this.myGroup.get('k'); }

  onSubmit() {
    console.log('Executed!')
    this.results = [];
    let k: number = this.k.value;
    let epsilon: number = 0.01;
    let guess: number = k / 2;
    let newGuess: number;
    while (Math.abs(guess * guess - k) >= epsilon) {
      newGuess = guess - (guess ** 2 - k) / (2 * guess);
      if (this.results.length == 0) {
        this.results.push(guess.toString() + ' => ' + newGuess.toString());
      } else {
        this.results.push(' => ' + newGuess.toString());
      }
      guess = newGuess;
    }
    this.guess = guess;
  }
}

 そして、コンポーネントのHTMLファイルです。

<p>
ニュートン-ラフソン法でkの根を求めます。
</p>
<p>
f(x) = x<sup>2</sup> - k<br>
x<sub>n + 1</sub> = x<sub>n</sub> - f(x) / f'(x) = x<sub>n</sub> - (x<sub>n</sub><sup>2</sup> - k)/2x<sub>n</sub>
</p>
<form [formGroup]="myGroup" (submit)="onSubmit()">
  <table>
    <tr>
      <th>k</th>
      <td><input type="number" formControlName="k"></td>
    </tr>
    <tr>
      <th></th>
       <td *ngIf="k.invalid" [style.color]="'red'">正の数を入力してください。</td>
    </tr>
    <tr>
      <th></th>
      <td>
        <input type="submit" value="Calc" [disabled]="myGroup.invalid">
      </td>
    </tr>
  </table>
</form>
<ul>
  <li>まず、guess = k / 2 = {{k.value}} / 2 = {{k.value / 2}} とします。<br>guess - (guess<sup>2</sup> - {{k.value}})/ (2 × guess)がより近い値になるので、計算結果の誤差が0.01になるまで繰り返します。<br>
    <p *ngIf="!results">Calcを押して続行します。</p></li>
  <li *ngFor="let ans of results">{{ans}}</li>
</ul>
<p *ngIf="guess">{{guess}} * {{guess}} = {{guess * guess}}</p>

WordPressへの組み込み

 stackblitz.comを見ていると、「Share」メニューがあったので、クリックして見ると、「Embed」タブが出てきましたので、WordPressに組み込めるのではないか、と、調べて組み込んでみました。

 組み込みブロックはあるのですが、stackblitz.com用のものがなく、調べているとここに書いてありました。iframeタグを使って、ということだったので、実験してみましたところ、以下のとおり、実行できました!

 ただ、編集のプレビュー画面では「Calc」ボタンは動きませんでした。iframeではsubmitが無効になっているようです。ただ、確認画面では動作しました。こちらの原因はまた機会があったら調べてみます。公開された後に実行できるかどうかわかりませんので、stackblitz.comのソースコードはこちら、実行結果はこちらです。

まとめ

 Angularのリアクティブフォームを使って、ページを作ってみました。

Angular始めました – stackblitz.comを使ったAngularの簡単な紹介

 今日も見に来てくださって、ありがとうございます。石川さんです。

 先日「Angularはじめました。」ということで、「AngularによるモダンWeb開発 基礎編 第2版」を読み込んで簡単な仕組みを作ってみよう、と、意気込んでいましたがずいぶん経ってしまいました。まずは、この書籍のサンプルをダウンロードして動作確認しようと取り組んでみたところ、もろもろ躓きまして。この本、初学者にはおすすめできませんね。あ、ぼくはすぐに何とか解決しましたよ。プロですからね。エヘン!でも、ちょっと初心者には難しかったかも知れない、と、追加の書籍を二冊購入して色々と勉強していました。学習効果が高いのはアウトプット、ということは分かっているのですが、どうにも本が好きなのでしょうねぇ。すぐに次の本、と買ってしまい割とコレクターみたいなところがあります。で、今日は、Angularってどんなものなの、ということを簡単に紹介したいと思います。そうそう、アウトプットです!

Angularとは何か、そのセットアップについて

 一言でAngularとは何か、というと、ステキなWebアプリを開発するためのフレームワーク、と言ってよいのかな、と、思います。ステキなWebアプリと書きましたが、流行の専門用語ではこれをPWA(Progressive Web Application)と呼んでいて、デスクトップアプリケーションのような体験ができるWebアプリ、ということのようです。これらはGoogleが開発を進めているオープンソースのフロントエンドフレームワークで、半年に一度はメジャーバージョンが更新される、という活発な開発状況です。書籍の数が比較的少ないのは、この頻繁なバージョンアップのせいではないか、ということをぼくは勝手に疑っています。

 ローカルで開発するためには、node.jsというjavascriptを実行するための環境を用意する必要があります。なので、まずはnode.jsをインストールします。node.jsのインストールが終わったら、そのnode.jsの中のインストールコマンドで、Angularをインストールするということになります。コマンドプロンプト、またはターミナルから以下のコマンドを実行してください。

npm install -g @angular/cli

これで、Angularがインストールされたことになります。その後、プロジェクトを作るために、任意のフォルダで以下のコマンドを実行することで、新しいプロジェクトのためのフォルダ(myProject)を作成します。

ng new myProject

開発は、ここから「ng serve」を実行して、作られた結果をブラウザで確認しながら、という感じで進められていくことになります。結構面倒ですよね。もっとお手軽にどんなものか知りたい、というAngular初心者の方々のために、stackblitz.comが便利なので紹介したいと思います。

stackblitz.comを使ってみましょう

 stackblitz.comはブラウザで利用できる統合開発環境(IDE)のクラウドサービスのひとつです。ホームページを見てみるとわかりますが、いくつかの開発に対応しています。今回はAngularですので、以下のアイコンを探してクリックしてみてください。

stackblitz.comのAngular開発環境入口

 すると、ブラウザの中に統合開発環境(IDE)がAngularの新規プロジェクトを開いた状態で開始します。左がフォルダとソースコードの一覧で、真ん中がソースコード、右がAngularを実行した結果、という感じです。

stackblits.comのAngular開発環境(初期画面)

 Angularでは基本的に、コンポーネントという部品の単位で開発することになります。初期表示では「app.component.ts」ファイルが開かれています。このファイルがコンポーネントの処理部分を担います。拡張子が「.html」のファイルが表示内容の骨格部分、「.css」が表示内容の飾りの部分、という感じで考えていただければ、と思います。

2022年2月3日 追記
 stackblitz.comにて本日再び新しいAngularプロジェクトを作成してみたところ、Angularのバージョンが13になっていました。保存してあるリポジトリもバージョンアップしましたので、以降の内容は12→13で読み替えるようお願いいたします。たしかこのバージョンは、半年に一回は更新されるんだよね。。。

 「Start editing to see some magic happen :)」と記載がありますので、ここの部分、さっそく直してみましょう。まずは、日本語使えるのかな、ということでこの部分を日本語にしてみます。左側の「app.component.html」ファイルをクリックして中央に開きます。英語部分を日本語にしてみましょう。

リアルタイムで修正が反映される

 HTMLファイルを編集すればわかりますが、修正と同時に右側のアプリケーション部分が更新されます。日本語もちゃんと表示されましたね。

画面遷移なしで使える機能を盛り込んでみた

 リアクティブフォーム以外で画面遷移しない範囲の簡単な項目をもろもろ盛り込んでみました。完全に自分用の備忘録です。忘れたときに参照しようと思って書いていますので、説明は省きたいと思います。出来上がりイメージは以下のとおりです。

できあがりイメージ

ソースコードは「app.components.ts」と「app.components.html」の二つだけ変更しました。以下の通りです。

import { Component, VERSION } from '@angular/core';

@Component({
  selector: 'my-app',
  templateUrl: './app.component.html',
  styleUrls: ['./app.component.css'],
})
export class AppComponent {
  name = 'Angular ' + VERSION.major;
  today = new Date();
  clicked: boolean;
  message: string;
  count: number = 0;
 list: string[] = [
    "AngularによるモダンWeb開発",
    "Angular超入門",
    "Angularデベロッパーズガイド",
  ];
  ch1: boolean = false;
  ch2: boolean = false;
  ch3: boolean = true;
  opt: string;
  sel: string;
  mul: string;

  onClick() {
    this.clicked = !this.clicked;
  }

  keyup(val: string) {
    this.message = val;
  }

  countup() {
    this.count++;
  }

}
<hello name="{{ name }}"></hello>
<p>
  編集を開始して、魔法が起こるのを確認してください☺
</p>
<ol>
  <li>変数が利用できます。name = 「{{name}}」</li>
  <li>計算もできます。3 + 3 = {{3 + 3}}</li>
  <li>日時を表示できます。{{today | date: "YYYY-MM-dd hh:mm:ss"}}</li>
  <li *ngIf="clicked" (click)="onClick()" [style.background-color]="'cyan'">クリックに反応します。A</li>
  <li *ngIf="!clicked" (click)="onClick()" [style.background-color]="'lightblue'">クリックに反応します。B</li>
  <li><input type="text" #f1 (keyup)="keyup(f1.value)">入力を下に反映します。</li>
  <li>↑の入力をここ「{{message}}」に瞬時に反映させます。</li>
  <li>{{count}}回クリックしました。<button (click)="countup()">Click</button></li>
  <li>ngForで繰り返し処理ができます。</li>
  <ol>
    <li *ngFor="let item of list;let i = index">{{item + ":i = " + i}}</li>
  </ol>
  <li>チェックボックスが使えます。
    <input type="checkbox" [(ngModel)]="ch1">
    <input type="checkbox" [(ngModel)]="ch2">
    <input type="checkbox" [(ngModel)]="ch3"></li>
  <ul>
  <li *ngIf="ch1">一つ目がチェックされています。</li>
  <li *ngIf="ch2">二つ目がチェックされています。</li>
  <li *ngIf="ch3">三つ目がチェックされています。</li>
  </ul>
  <li>ラジオボタンが使えます。
    <label><input type="radio" [(ngModel)]="opt" value="男">男</label>
    <label><input type="radio" [(ngModel)]="opt" value="女">女</label>
    <label><input type="radio" [(ngModel)]="opt" value="不明">不明</label></li>
  <ul>選択値は「{{opt}}」です。</ul>
  <li>プルダウンが使えます。
  <select [(ngModel)]="sel">
    <option *ngFor="let item of list">{{item}}</option>
  </select></li>
  <ul>選択値は「{{sel}}」です。</ul>
  <li>複数選択リストが使えます。
    <select [(ngModel)]="mul" multiple size=3>
      <option *ngFor="let item of list">{{item}}</option>
    </select>
  </li>
  <ul>選択値は「{{mul}}」です。</ul>
</ol>

stackblitz.comとgithub.comで共有できます

  stackblitz.comは、github.comと連携することでソースコードを保存することができます。そして、保存すると他の人と共有することが可能になります。今回はこちらがソースコードの共有です。そして、こちらが実行結果の共有です。

まとめ

 Angularを使うと、これまで簡単にできなかったことが、そこそこ簡単にできるようになりました。

Angular始めました – モダンWeb開発 PWA(Progressive Web Application)始めますよ~!

 今日も見に来てくださって、ありがとうございます。石川さんです。

 ご縁があって、近々Webシステムの開発をすることになるかも知れない、ということになりましたので、諸々検討して、Angularの勉強を始めることにしました。

Angularによる モダンWeb開発

 Webシステムが出始めた当時、4GLの開発をしていたこともあって、あの複雑なだけで開発生産性が高くもなく、操作性の少しも良くない仕組みがどうしても好きになれず、ずっと避けてきていたのですよね。そう、もっさりとしていて遅いし、ユーザーインターフェースも雑で細やかさがなくて、嫌だったのですよね。アプリケーションのインストールがない、というメリット以外には良さを感じられなかったのです。様子が変わってきたのはGoogle Mapあたりからで、それまでのWebアプリケーションらしからぬ動きはかなりの衝撃でしたね。そして、GmailにGoogleドキュメントと、かつてのWebアプリケーションとは違う感じになってきているなぁ、というのは知っていましたが、とうとうWebアプリケーションに手を出す決意をしました。

 と、いうのも、Angularのホームページのチュートリアルに感動して、これは本格的に取り組まねば、という気持ちになりました。まず、実行できるチュートリアルがすべてWebで完結しているのですよね。Visual Studio CodeのようなIDEが自動的に開始します。先日gitPodでも体験していましたがブラウザだけで開発できるのは素晴らしいですね!そして、コンポーネント指向開発という新しい開発方法もなかなか面白いですね。部品が多くなり過ぎたら再利用が難しくなるかも知れないなぁ、という気持ちになりましたが、もうちょっとやれば、何かつかめるかも知れませんね。

 それと、Comppassの「Angular日本ユーザー会のYoutubeライブ!」を視聴しました。予想通りですが、まったくついていけませんでした!まだ開発もしてないですからねぇ。Angularのバージョン13リリースに関して、追加、更新、削除された機能について、語られていました。現状がまったくわからないので、まあついていけないのは当然ですね。参加して、引っ掛かりができるようになれば、という感じです。

 Angularのホームページと、画像の書籍「AngularによるモダンWeb開発 基礎編 第2版」を読み込んだら、簡単な仕組みを作ってみようと思います。

 さあ、がんばるぞー!

実行時エラー’5′:プロシージャの呼び出し、または引数が不正です。

 今日も見に来てくださって、ありがとうございます。石川さんです。
Excelの実行時エラー、1年ほど前に書いた記事のアクセスが一番多いので、みなさんのお役に立っているようです。と、いうことで先日職場にて発生した実行時エラー、今度は「5」です。こちらが発生しました。今回は全くつまづくこともなく、すぐに解決できたのですが、ひとによっては悩みに悩んで解決できない、ということもあるかも知れないと思って記事にすることにしました。(Excelはニーズが多いですからね。アクセスが増えるかもしれない、と調子に乗っております。)

実行時エラー ‘5’:プロシージャの呼び出し、または引数が不正です。

 急いでいる方に結論だけ、お知らせします。こちらは以下のスクリプトで再現できます。

Sub test()
    'B1セルの内容がA1セルより小さいとき、B1セルの背景をグレーにしてフォントを太字にする条件付き書式を設定します
    With Range("B1").FormatConditions.Add(Type:=xlExpression, Formula1:="=AND($B1<$A$1)")
        .Font.Bold = True
        .Interior.Color = RGB(166, 166, 166)
    End With
End Sub

 ちなみにこちらのスクリプト、以前は問題なく実行できていたのですけど、ある日突然「実行時エラー’5’」が発生するようになりました、というお問い合わせでした。ただ、こちらのスクリプト、メールで受信して、ダウンロードして実行しても、ぼくの環境ではエラーにならなかったのです。不思議でしょ。なので、上記のスクリプトを記述してもエラーにならない方もいるかも知れません。その場合は、違う原因かも知れません。

 それで、エラーの出ている人のエクセルとぼくのエクセルをよくよくを見てみますと、セルの列の表記が違うのですよね。

エラーが出ている方のエクセルの列の表記
エラーが発生していないエクセルの列の表記

 そう、列が「1、2、3、4、5、、、」となっているか「A、B、C、D、E、、、」となっているかの違いです。こちらは「ファイル(F)」の「オプション」を選択することで表示される「Excel のオプション」画面から「数式」を選択することで表示される「数式の処理」の「R1C1 参照形式を使用する(R)」をチェックすることで切り替えられます。チェック時は「1、2、3、4、5、、、」で、チェックオフ時は「 A、B、C、D、E、、、」と表示されるようになります。

 今回はこの設定が悪さをしていました。スクリプト作成時はR1C1参照形式を使わない設定になっていたのに、途中で切り替えて保存した、ということのようです。こちらの「R1C1 参照形式を使用する(R)」の設定は、Excel全体の設定ではなくて、ファイルごとに設定を保持するようです。ちょっと動作確認してみたところ、新規作成時は前回エクセル終了時の設定を利用して、既存ファイルを開いたときはそのファイルの設定を利用するようです。

まとめ

 マクロ実行時に今まで動作していたのにこのエラーが出るようになったら、「R1C1 参照形式を使用する(R)」の設定を疑ってみるといいかも知れませんね。マクロで条件付き書式の設定をするときには、参照形式をこの設定にそろえておかないといけない、ということですね。

Python プログラミング Anaconda3 再起動後、なぜか Spyder が起動できない!

 いつも見にきてくれてありがとうございます。石川さんです。スイカの季節ですね。いや、今ならオリンピックの話題が優先でしょうか。オリンピック、あんまり興味が持てないんだよねぇ。せっかく東京でやるのにね。

概要

 Anacondaに付属してくるSpyder IDEを使っていたのですが、パソコン再起動後、突然起動しなくなりました。原因はプロジェクトを利用するときに、日本語のファイル名を使ったことでした。Bugのようですが、対応方法について記載しておきました。

経緯

 最近、Pythonのスクリプトの編集、実行には、AnacondaをインストールするとついてくるSpyderを使っています。しかし、今日は、パソコンを再起動したあとに、Spyderを起動するとなぜか立ち上がらなくなってしまいました。えっ、どうした、Spyderよ。立ち上がって来いよ~。そんなに貧弱じゃないだろ?と、言いつつ、Anacondaプロンプトですらヒストリが読めなくて起動時にエラーを出力していたことを思い出しました。何か、入力しちゃいけない文字コードを入力してしまったのでしょうか。しかし、起動した後に、スプラッシュ画像が出て、そのままお亡くなりになるなんて、聞いてないよ~。少なくともエラーメッセージを出して欲しいよ。どうしようもないじゃない、Spyderさん。

まずはSpyderを開始できるようにします

 さて。気を取り直して、実行プログラムのありかを探して、まずはそこからでしょうか、と、思って実行プログラムを見ると、おや、今まで気にもならなかったプログラムがあるじゃないですか!そう、「Reset Spyder Settings (anaconda3)」です。こんなの、今まで、気にしたこともなかったなぁ。

Reset Spyder Settings(anaconda3)

 さて、実行したところ、何やら設定がリセットされたような気がしますので、再度実行してみます。心なしかスプラッシュ画像が小さくなったような気がします。何とか立ち上がりました。アイコンも心なしか小さくなったような気がしますねぇ。。。ま、それはいいか。

アイコンが小さくなったような気がする

原因を追究します

 直前に作業していたのは、、、と、記憶をたどると、そうそう、新しくプロジェクトを作成して、そこにドキュメントフォルダをつくって、Sphinxのドキュメントを構築しようとしていたんだった、と、いうことを思い出しました。ということで、その新しいプロジェクトを再度開いてみると、、、おお、エラーか?。。。エラーですね。「Issue reporter」なる画面が起動されました。ふむふむ「Spyder has encountered an internal problem!」ということですね。内部の問題に直面しました、ということなのですが、問題レポートを送ればいいのかな?

Issue reporter

 いや、送る前に、包括的なトラブルシューティングガイドで相談してください、と、書いてありますね。ほとんどの問題が解決されるらしいです。あとは、分かっているバグ、を検索してということだけど、検索しようにも何のメッセージも出てないしなぁ。。。そもそもこの「Issue report」は出てきたんだけど、エラーメッセージは何にも出力されていないのですよね。だからレポートを書くとしたら、「プロジェクトを開こうとしたら、内部的な問題に出会いました」くらいしか書きようがないですよね。とりあえず、「Close」でよいのかな?あ、「Close」ボタンの隣に「Show details」ボタンを発見しました!このボタンを押して詳細を見てみよう。と、ポチリとした結果、下部にエラーメッセージが出てまいりました。

Show detailsを押した結果

テキストは一部分だけが見える状態でしたので、以下に全文を張り付けておきます。これだけ情報があれば、なんとかなりそうですね。

Traceback (most recent call last):
  File "C:\Users\mitsu\anaconda3\lib\site-packages\spyder\plugins\projects\plugin.py", line 129, in <lambda>
    triggered=lambda v: self.open_project())
  File "C:\Users\mitsu\anaconda3\lib\site-packages\spyder\plugins\projects\plugin.py", line 402, in open_project
    project_type_class = self._load_project_type_class(path)
  File "C:\Users\mitsu\anaconda3\lib\site-packages\spyder\plugins\projects\plugin.py", line 813, in _load_project_type_class
    config.read(fpath)
  File "C:\Users\mitsu\anaconda3\lib\configparser.py", line 697, in read
    self._read(fp, filename)
  File "C:\Users\mitsu\anaconda3\lib\configparser.py", line 1017, in _read
    for lineno, line in enumerate(fp, start=1):
UnicodeDecodeError: 'cp932' codec can't decode byte 0x81 in position 269: illegal multibyte sequence

 想定の範囲内ですが、UnicodeDecodeErrorですね!記憶にないけど、プロジェクト作るときに、日本語を使っちゃったのかなぁ。configparser.pyでエラーが発生しているのと、起動時は問題なくてプロジェクトを開くときにエラーが発生したので、プロジェクトのフォルダを見に行ってみます。ありましたね「.spyproject」フォルダが怪しいです。ちょっと覗いてみます。

エラーになったプロジェクトのフォルダ

 なるほど、「config」フォルダがあって、その中に複数の「.ini」ファイルがあります。この中をひとつずつ見ていきましょう。

エラーになったconfigフォルダ

 見つけました。「workspace.ini」ファイルですね。ここにマルチバイト文字がありました。そうそう、Sphinxで使う「.rst」のファイル名を日本語にしたのを思い出しました。このファイル自体はUTF-8で保存されていたので、うまく開けないのはどうしてでしょうね。

[workspace]
restore_data_on_startup = True
save_data_on_exit = True
save_history = True
save_non_project_files = False
project_type = empty-project-type
recent_files = ['..\\..\\..\\Users\\mitsu\\.spyder-py3\\temp.py', 'doc\\source\\index.rst', 'doc\\source\\このドキュメントについて.rst', 'doc\\source\\conf.py']

[main]
version = 0.2.0
recent_files = []

さらに原因を追究します

 configparser.pyのソースファイルを覗いてみたところ、ファイルをencoding=Noneとしてオープンしていることがわかりました。encoding=Noneとした場合は、ロケールのデフォルトエンコーディングが使用されます。つまり、日本語のWindows10環境では「cp932」が使われる、ということになります。configparser.pyの中にはファイルへの書き込みのためのオープンが見当たらなかったので、どのような設定になっているのかはちょっと不明だったのですが、上述のとおりファイルの内容はUTF-8で書き込まれていたので、書き込み時はUTF-8でオープンされているのだと思います。

では、修正しましょう

 さて、どう修正するのがよいでしょうか。気持ちとしてはconfigparser.pyのencoding=”utf-8″としたいところですが、影響範囲がはかり知れないので、Spyderに限って考えたいと思います。そうすると、エラーメッセージで指摘されている、C:\Users\mitsu\anaconda3\lib\site-packages\spyder\plugins\projects\plugin.pyの813行目に、encoding=”utf-8″を追加するのがよいでしょうか。

        config.read(fpath)  # 修正前
        config.read(fpath,encoding="utf-8")  # 修正後

 さて、修正してみたまではよかったのですが、果たして反映されるのでしょうか。Spyderって、実行形式で配布されている訳じゃないのかな?と、迷っている間に実行できそうなので、ささっとSpyderを再起動してみます。おっ、できました。エラーは発生しませんでした。

エラーなく日本語ファイルが開けました

まとめ

 結論としては、UTF-8で保存したファイルをローカルロケールでオープンして読み込んだためにエラーが発生した、ということでしたね。なので、パソコン再起動が原因ではなくて、Spyderの再起動が原因、ということでした。Pythonはバージョン3から完全UTF-8対応したはずなのに、どうしてこんなことが起きるのかというと、デフォルトエンコーディング、大混乱しているようですね。「Fluent Python Pythonicな思考とコーディング手法」を参考に、Pythonのデフォルトエンコーディングに影響を与える設定を確認してみました。

Python 3.8.8 (default, Apr 13 2021, 15:08:03) [MSC v.1916 64 bit (AMD64)]
Type "copyright", "credits" or "license" for more information.

IPython 7.22.0 -- An enhanced Interactive Python.

In [1]: import sys, locale

In [2]: locale.getpreferredencoding()
Out[2]: 'cp932'

In [3]: my_file = open("dummy","w")

In [4]: type(my_file)
Out[4]: _io.TextIOWrapper

In [5]: my_file.encoding
Out[5]: 'cp932'

In [6]: sys.stdout.isatty()
Out[6]: False

In [7]: sys.stdout.encoding
Out[7]: 'UTF-8'

In [8]: sys.stdin.isatty()
Out[8]: False

In [9]: sys.stdin.encoding
Out[9]: 'cp932'

In [10]: sys.stderr.isatty()
Out[10]: False

In [11]: sys.stderr.encoding
Out[11]: 'UTF-8'

In [12]: sys.getdefaultencoding()
Out[12]: 'utf-8'

In [13]: sys.getfilesystemencoding()
Out[13]: 'utf-8'

In [14]: my_file.close()

 書籍はブラジル向けにローカライズされたWindows 7の結果が示されており、結果はなんと4種類のエンコーディングが出力されていました。それに比べると日本語のWindows10環境は「cp932」と「utf-8」の二つしかないので、まだマシな方かも知れませんね。(苦笑)

Python プログラミング Anaconda3でのpytestの警告を回避、iPad Proの a-Shell で pytest 始めてみました

 今日も見にきてくださってありがとうございます。石川さんです。梅雨入りしてジメジメが続きますね。

 さて、先日翔泳社の『テスト駆動Python』という書籍を購入しまして、さ〜て、やりますか、と張り切っていたのですが、1章の1ページ目からうまく動かず。実行はできたのですけど、何故か警告が出力されてしまうのですよね。ぼくの環境は、Windows 10 & Anaconda3です。

(base) C:\work\pytest\ch1>pytest test_one.py
================================================= test session starts =================================================
platform win32 -- Python 3.8.8, pytest-6.2.3, py-1.10.0, pluggy-0.13.1
rootdir: C:\work\pytest\ch1
plugins: anyio-2.2.0
collected 1 item

test_one.py .                                                                                                    [100%]

================================================== warnings summary ===================================================
..\..\..\ProgramData\Anaconda3\lib\site-packages\pyreadline\py3k_compat.py:8
  C:\ProgramData\Anaconda3\lib\site-packages\pyreadline\py3k_compat.py:8: DeprecationWarning: Using or importing the ABCs from 'collections' instead of from 'collections.abc' is deprecated since Python 3.3, and in 3.9 it will stop working
    return isinstance(x, collections.Callable)

-- Docs: https://docs.pytest.org/en/stable/warnings.html
============================================ 1 passed, 1 warning in 0.08s =============================================

(base) C:\work\pytest\ch1>
警告が出力される

 なるほど、Anaconda3のpyreadlineモジュールで警告ですね。これは問題だと言われているファイル、py3k_compat.pyの8行目、「return isinstance(x, collections.Callable)」のところの「collections.Callable」を「collections.abc.Callable」に修正することで回避できました。そういえば、ちょっと前にもこちらの記事でpyreadlineのモジュール、修正していましたね。警告が出なくなってスッキリしたのですが、ふと、最近購入した、iPad Proでもできるんじゃないかな、と、やってみました。

実行環境のセットアップ

 iPad Proでのpythonというと、大体は「Pythonista 3」か、jupyter notebook系だと「Juno」「Carnets」がおすすめされることがほとんどだと思います。でも、今回使ったのは、「a-Shell」というアプリで、iOSで簡単なUnixコマンドが使えるシェルです。ここから「python」や「ipython 」を実行することができます。App Storeの検索で「a-Shell」を入力してぐるぐるのアイコンを探してインストールします。

a-Shellのアイコン

 インストールできたら、実行します。黒いコマンドラインがフルスクリーンで登場します。pytestを実行してみましたが、コマンドが存在しない(command not found)、と、エラーになります。pytestを使えるようにするためには、インストールが必要ですね。pipコマンド「pip install pytest」で簡単に実行できるようになりました。

$ pytest
pytest: command not found
$ pip install pytest
Defaulting to user installation because normal site-packages is not writeable
Collecting pytest
  Downloading pytest-6.2.4-py3-none-any.whl (280 kB)
     |████████████████████████████████| 280 kB 4.0 MB/s 
Requirement already satisfied: packaging in /private/var/containers/Bundle/Application/10B03820-2240-4A08-AA93-CE205988D553/a-Shell.app/Library/lib/python3.9/site-packages (from pytest) (20.9)
Requirement already satisfied: attrs>=19.2.0 in /private/var/containers/Bundle/Application/10B03820-2240-4A08-AA93-CE205988D553/a-Shell.app/Library/lib/
python3.9/site-packages (from pytest) (20.3.0)
Requirement already satisfied: toml in /private/var/containers/Bundle/Application/10B03820-2240-4A08-AA93-CE205988D553/a-Shell.app/Library/lib/python3.9/site-packages (from pytest) (0.10.2)
Collecting py>=1.8.2
  Downloading py-1.10.0-py2.py3-none-any.whl (97 kB)
     |████████████████████████████████| 97 kB 10.5 MB/s 
Collecting iniconfig
  Downloading iniconfig-1.1.1-py2.py3-none-any.whl (5.0 kB)
Collecting pluggy<1.0.0a1,>=0.12
  Downloading pluggy-0.13.1-py2.py3-none-any.whl (18 kB)
Requirement already satisfied: pyparsing>=2.0.2 in /private/var/containers/Bundle/Application/10B03820-2240-4A08-AA93-CE205988D553/a-Shell.app/Library/lib/python3.9/site-packages (from packaging->pytest) (2.4.7)
Installing collected packages: py, pluggy, iniconfig, pytest
Successfully installed iniconfig-1.1.1 pluggy-0.13.1 py-1.10.0 pytest-6.2.4

実行してみます

  書籍の内容を元に、test_one.pyファイルを作成して、以下の通り実行できました。

$ pytest test_one.py
================================================================= test session starts ==================================================================
platform darwin -- Python 3.9.2+, pytest-6.2.4, py-1.10.0, pluggy-0.13.1
rootdir: /private/var/mobile/Containers/Data/Application/117A9320-8100-4E66-A829-2B31D0457140/Documents/work/pytest/ch1
collected 1 item                                                                                                                                       
test_one.py .                                                                                                                                    [100%]
================================================================== 1 passed in 0.01s ===================================================================
$ 
実行結果

はい、見事に成功しましたね。pytestでテスト対象として自動的に実行されるためには、

  • ファイル名が test_*.py または、*_test.py という名前であること
  • 関数名やメソッド名は、test_* という名前であること
  • クラス名は Test* という名前であること

ということのようです。最初、テスト関数名を「est_passing」と「t」を欠落して作ってしまっていました。その欠落が原因で実行されなかったのですが、「t」の欠落に気づくまでは、警告があると実行できないのか〜、と勘違いしてしばらくウロウロしちゃいました。無事、解決できてよかったです。

お気に入り

 最近は、iPad Pro がお気に入りで、色々と遊んでいます。手書きの性能がとっても良いのが特にお気に入りで、近頃は手帳の替わりにノートアプリの「Goodnotes5」を使うようになりました。手書きのいいところと、電子のいいところの両方が享受できていて、大満足です。
 で、せっかく持ち歩けるのだから、pythonを使ったプログラミングもできないかなぁ、と、色々と検索してたどり着いたのが、今回紹介した「a-Shell」です。ただ、こちら tkinter が使えないので、しばらくは使い道がないなぁ、と思っていたのですが、GUIを利用しない今回のようなケースであれば活用できるはずだ、と、今回張り切って記事にしてみました。

まとめ

 Anaconda3 で pytest を実行するときにでた警告の修正と、iPad Proで pythonを使って pytest を実行する方法について記載しました。これでしばらくは気軽に pytest で遊ぶことができそうです。
 ちなみに、今回の記事は、iPad Pro を使って書いてみました。って、どこ見てもそんなこと分かりませんね。失礼しました!

Python プログラミング Windowsショートカットの参照先サーバ名の変更

 見に来てくださって、ありがとうございます。石川さんです。

 たくさんあったWindowsのショートカットの参照先をスクリプトを使って一気に変えたくなったのですが、やり方がすぐに見つけられなかったので記録しておきます。サーバのリプレースやフォルダの整理等により、ショートカットの参照先が変わってしまって対応が必要になったときに使えます。どなたかのお役に立てば、うれしいです。

 すぐに見つけられなかったのは、「ショートカット」という名称がよくなかったようです。キーボードショートカットが検索結果の上位に登場してきます。試行錯誤して探し出しましたが、まずは参考になったのがこちらです。ここでショートカットの拡張子は「.lnk」らしい、ということと、win32com.clientのDispatchで”WScript.Shell”を使えばショートカットが作成できるらしい、と、アタリをつけました。
 今回は、ショートカットのプロパティの「リンク先(T)」「作業フォルダー(S)」を修正したかったのですが、上記で見つけたリンクでは「リンク先(T)」が「TargetPath」だ、ということしかわからなかったので、追加で「WScript.Shell」+「shortcut」+「property」で検索してこちらのページを見つけました。こちらで「作業フォルダー(S)」は「WorkingDirectory」ということがわかりました。

ショートカットのプロパティ

ソースコード

 ここまでわかればなんとかなる、ということでスクリプトを作成しました。Pythonです。

#!python
# -*- coding: cp932 -*-
# このスクリプトは指定したフォルダの中をくまなく操作して、
# ショートカットを見つけたときに、「置き換え前」を「置き換え後」に置き換えるためにつくりました。
# 変数を変更することで他の用途にも対応可能です。

import os
import win32com.client 

指定フォルダ = "Z:" + os.sep + "テストフォルダ"
置き換え前 = r"\\old_server" + os.sep + "shared_folder$"
置き換え後 = r"\\new_server" + os.sep + "shared_folder$"

shell = win32com.client.Dispatch("WScript.Shell")

for root, dirs, files in os.walk(指定フォルダ):
    for file in files:
        if file[-4:] == ".lnk":
            shortcut = shell.CreateShortCut(root + os.sep + file)
            if shortcut.Targetpath[:len(置き換え前)].lower() == 置き換え前.lower():
                changed_targetpath = 置き換え後 + shortcut.Targetpath[len(置き換え前):]
                shortcut.Targetpath = changed_targetpath
                shortcut.save()
            if shortcut.Workingdirectory[:len(置き換え前)].lower() == 置き換え前.lower():
                changed_workingdirectory = 置き換え後 + shortcut.Workingdirectory[len(置き換え前):]
                shortcut.Workingdirectory = changed_workingdirectory
                shortcut.save()

 まずは、一番のポイント、14行目のWScript.Shellですね。ここではCOMオブジェクトをディスパッチしています。これによりWindowsのWshShellオブジェクトの機能が利用可能になります。
 次は16行目ですが、os.walkを利用して、「指定フォルダ」配下のファイルをすべてチェックします。ここのforループのrootに、現在処理対象のフォルダ名、filesにそのフォルダ内にあるファイルのリストが取得できます。この取得したファイルリストをすべて処理し始めるのが、17行目のforループです。
 18行目では、ファイル名の最後の4文字を取り出して「.lnk」かどうかチェックしています。ショートカットは拡張子が「.lnk」になっています。そして、ショートカットの場合、19行目で「CreateShortCut」を使ってショートカットのオブジェクトを作成しています。作成する、と言っても元々がショートカットですので、ショートカットのショートカットが新しく作成されるわけではなく、現在の設定のショートカットを参照した形になります。ちなみに、os.sepは、セパレート文字列で、Windowsの場合「\」、Linuxの場合は「/」のように適切な区切り文字にセットされます。
 20行目で置き換え対象かどうかチェックして、対象なら「Targetpath」をセットすることで「リンク先」を更新して23行目の「save」メソッドで変更を確定する、ということをやっています。24~27行は同様ですが、「WorkingDirectory」をセットすることで「作業フォルダー」を更新しています。

まとめ

  やりたいことは難しいことでなかったのですが、既存のショートカットを更新したいのに「CreateShortCut」メソッドを使う、というところがちょっと馴染めない感じがしました。ま、慣れれば大丈夫ですね。

Python GUIプログラミング tkinterのクラス

 今日も見に来てくださってありがとうございます。石川さんです。久しぶりの更新です。

ちょうど一年くらい前にPythonでGUIという記事を書きました。いくつかサンプルをつくって勉強していくうちに色々とできるようなって、tkinterで使っているクラスの継承関係をいっちょ描いてみるか、ということでプログラミングしてみました。早く結論を知りたい方のために、まずはできあがりイメージの画像です。

できあがりイメージ

tkinter承継関係

ソースコード

 ソースコードは以下の通りです。

import tkinter as tk
import inspect


class Class:
    '''クラスを描画するためのクラス'''
    CLASS_COUNTER = 0

    def __init__(self, canvas, name):
        self.canvas = canvas
        self.name = name
        self.start_pos = None
        self.connections = []
        Class.CLASS_COUNTER += 1

        x = 150 + 200 * (Class.CLASS_COUNTER % 5)
        y = 150 + 50 * (Class.CLASS_COUNTER // 5)
        text = canvas.create_text(x, y, text=name, tag=name)
        rect = canvas.create_rectangle(canvas.bbox(name), tag=name, fill="lightyellow")
        canvas.tag_lower(rect, text)
        canvas.tag_bind(name, "<ButtonPress>", self.start)
        canvas.tag_bind(name, "<Motion>", self.move)
        canvas.tag_bind(name, "<ButtonRelease>", self.end)

    def start(self, event):
        '''クラスをクリックされたときのメソッド'''
        self.start_pos = (self.canvas.canvasx(event.x),
                          self.canvas.canvasy(event.y))

    def move(self, event):
        '''クラスを動かすためのメソッド'''
        if self.start_pos is None:
            return
        x, y = self.canvas.canvasx(event.x), self.canvas.canvasy(event.y)
        dx, dy = x - self.start_pos[0], y - self.start_pos[1]
        self.canvas.move(self.name, dx, dy)
        self.start_pos = x, y
        for connection in self.connections:
            connection.move(self)

    def end(self, event):  # pylint: disable=unused-argument
        '''クラスの選択が終わった時のメソッド'''
        self.start_pos = None

    def get_center(self):
        '''中心点を戻す'''
        bbox = self.canvas.bbox(self.name)
        return (bbox[0] + bbox[2]) // 2, (bbox[1] + bbox[3]) // 2

    @property
    def x(self):
        '''左上のx座標を戻す'''
        bbox = self.canvas.bbox(self.name)
        return bbox[0]

    @property
    def y(self):
        '''左上のy座標を戻す'''
        bbox = self.canvas.bbox(self.name)
        return bbox[1]

    @property
    def height(self):
        '''高さを戻す'''
        bbox = self.canvas.bbox(self.name)
        return abs(bbox[1] - bbox[3])

    @property
    def width(self):
        '''幅を戻す'''
        bbox = self.canvas.bbox(self.name)
        return abs(bbox[0] - bbox[2])

    def add_listener(self, connection):
        ''' コネクションのリスナーを登録します '''
        self.connections.append(connection)


class Inheritance:
    '''継承関係の線を表示するためのクラス'''
    def __init__(self, canvas, parent, child):
        self.canvas = canvas
        self.parent = parent
        self.child = child
        parent.add_listener(self)
        child.add_listener(self)

        p_coords = self.get_intersection(parent)
        c_coords = self.get_intersection(child)
        self.id = self.canvas.create_line(*p_coords, *c_coords, arrow=tk.FIRST)

    def get_intersection(self, box):
        ''' 矩形との接点を求める '''
        x, y = box.get_center()
        height, width = box.height // 2, box.width // 2
        dx, dy = self.child.x - self.parent.x, self.child.y - self.parent.y
        if box == self.child:
            dx, dy = -dx, -dy
        if dx == 0:
            dx = 0.001
        if abs(dy / dx) < (height / width):  # 垂直側
            x_pos = x + width if dx > 0 else x - width
            y_pos = y + dy * width / abs(dx)
        else:  # 水平側
            x_pos = x + dx * height / abs(dy)
            y_pos = y + height if dy > 0 else y - height
        return x_pos, y_pos

    def move(self, box):
        ''' エンティティが移動したときの処理(エンティティから呼び出される)'''
        coords = self.canvas.coords(self.id)
        if box == self.parent:
            coords[0:2] = self.get_intersection(box)
            coords[2:4] = self.get_intersection(self.child)
        elif box == self.child:
            coords[0:2] = self.get_intersection(self.parent)
            coords[2:4] = self.get_intersection(box)
        self.canvas.coords(self.id, coords)


class Application:
    '''アプリケーションクラス'''
    def __init__(self, root):
        self.root = root
        root.title("tkinter classes")
        root.geometry("1200x800")
        self.start_pos = None
        self.item = None

        self.canvas = tk.Canvas(root, background="white")
        self.canvas.pack(fill=tk.BOTH, expand=True)
        self.canvas.bind("<Motion>", self.move)

        self.classes = {}
        self.init()

    def init(self):
        '''初期化'''
        for name, obj in inspect.getmembers(tk):
            if inspect.isclass(obj):
                if name in ('getdouble', 'getint', '_setit'):
                    continue
                if name not in self.classes.keys():
                    self.classes[name] = Class(self.canvas, name)
                for parent in obj.__bases__:
                    p_name = parent.__name__
                    if p_name == 'object':
                        continue
                    if p_name not in self.classes.keys():
                        self.classes[p_name] = Class(self.canvas, p_name)
                    print(p_name, "<--", name)
                    Inheritance(self.canvas, self.classes[p_name], self.classes[name])

    def move(self, event):
        '''マウスが動いたときにタイトルに座標を表示します'''
        title = "tkinter class[" + str(event.x) + "," + str(event.y) + "]"
        self.root.title(title)


def main():
    '''主処理'''
    root = tk.Tk()
    application = Application(root)
    application.root.mainloop()


if __name__ == "__main__":
    main()

詳細説明

 最初のポイントは、inspectモジュールを使って、tkinterモジュールの中身を検査しているところでしょうか(139行~)。メンバーを順番に取り出してクラスを抜き出します。実行してみてから小文字のクラスがあるのに気づいたのですが、これらはint、floatに別名を付けていたものと内部的なクラス(Internal class)とコメントに書いてあったので、出力されないようスキップしました。
 一度も出力していないクラスを出力して、その親クラスも同様に出力、その後承継を作成するようにしています。

 その他の部分は、これまでに記事にした内容を参照すれば、できそうですね。

まとめ

 tkinterのクラス図ができあがりました。本当は、レイアウトもある程度自動でやりたかったのですが、とりあえず、今回は、手で移動しました。もうちょっと、勉強してきます。

Python GUIプログラミング Windows実行形式ファイルをつくる

 今日も見に来てくださって、ありがとうございます。石川さんです。

 今回は、Pythonスクリプトから実行形式の「.exe」ファイルをつくる、というのを書きたいと思います。ずいぶん昔にもお試しでやったことがあったのですけど、どうやるのか、すっかり忘れてしまったので、備忘録的なアレですね。

迷いどころ

 実行形式のファイルをつくる方法、ちょっと検索しただけで、山のようにあるのですね。どれを使ったらよいのか、迷います。手元の書籍には、cx_Freezeが紹介されていましたので、それで進めるのがいいのかな、と思っていましたが、最終的には、pyInstallerを使うことに決めました。今はAnaconda3を使っているので、そこに入っているやつがいいなぁ、と調べてみたところ、pyinstallerならcondaコマンドでインストールできそうだったのです。これが決め手ですね。

(base) C:\work>conda search pyinstaller
Loading channels: done
# Name                       Version           Build  Channel
pyinstaller                      3.4  py27h7a46e7a_1  pkgs/main
pyinstaller                      3.4  py36h2a8f88b_1  pkgs/main
pyinstaller                      3.4  py37h2a8f88b_1  pkgs/main
pyinstaller                      3.5  py27h7a46e7a_0  pkgs/main
pyinstaller                      3.5  py36h2a8f88b_0  pkgs/main
pyinstaller                      3.5  py37h2a8f88b_0  pkgs/main
pyinstaller                      3.6  py36h2a8f88b_1  pkgs/main
pyinstaller                      3.6  py36h2a8f88b_2  pkgs/main
pyinstaller                      3.6  py36h62dcd97_4  pkgs/main
pyinstaller                      3.6  py36h62dcd97_5  pkgs/main
pyinstaller                      3.6  py37h2a8f88b_1  pkgs/main
pyinstaller                      3.6  py37h2a8f88b_2  pkgs/main
pyinstaller                      3.6  py37h62dcd97_4  pkgs/main
pyinstaller                      3.6  py37h62dcd97_5  pkgs/main
pyinstaller                      3.6  py38h2a8f88b_1  pkgs/main
pyinstaller                      3.6  py38h2a8f88b_2  pkgs/main
pyinstaller                      3.6  py38h62dcd97_4  pkgs/main
pyinstaller                      3.6  py38h62dcd97_5  pkgs/main

(base) C:\work>

 そう、他の、py2exe、cx_Freeze(cx-Freeze)、pyoxidizer、bbFreeze、py2app、Shiv、PyRun、pynsist、は、ことごとくダメだったのですよねぇ。ま、condaのオプションを変更して登録先を変更すれば見つけられそうでしたけど、デフォルトで入っていない、ということはちょっぴり信頼性が足りないのかな、という理由で候補から外すことにしました。いろいろとウロウロしている中で、Nuitka、Bazelというのはconda searchコマンドで見つけることができたのですけど、あんまりメジャーじゃなさそう、ということで、今回はパスすることにしました。はい、ぜんぜん検証まではしていませんので、ご了承くださいね。

(base) C:\work>conda search py2exe
Loading channels: done
No match found for: py2exe. Search: *py2exe*

PackagesNotFoundError: The following packages are not available from current channels:

  - py2exe

Current channels:

  - https://repo.anaconda.com/pkgs/main/win-64
  - https://repo.anaconda.com/pkgs/main/noarch
  - https://repo.anaconda.com/pkgs/r/win-64
  - https://repo.anaconda.com/pkgs/r/noarch
  - https://repo.anaconda.com/pkgs/msys2/win-64
  - https://repo.anaconda.com/pkgs/msys2/noarch

To search for alternate channels that may provide the conda package you're
looking for, navigate to

    https://anaconda.org

and use the search bar at the top of the page.



(base) C:\work>

セットアップ

 早速ですが、PyInstallerをセットアップしていきます。公式ホームページには、以下のコマンドでインストールできる、と書いてありました。

pip install pyinstaller

 が、ぼくの使用している環境はAnaconda3ですので、condaを使ってセットアップします。コマンドは以下の通りです。

conda install pyinstaller

condaも調べていくと「-c」オプションでチャンネルを追加すれば、いろんなところからインストールできることがわかりました。conda-forgeチャンネルを指定すればもう少し新しいPyInstallerがインストールできることも分かったのですが、ここはあえてシンプルなやり方を使うことにしました。(condaにはconfigコマンドもあって、どのチャンネルからインストールするかの設定も変えられるのですね。)

(base) C:\work>conda install pyinstaller
Collecting package metadata (repodata.json): done
Solving environment: done

## Package Plan ##

  environment location: C:\ProgramData\Anaconda3

  added / updated specs:
    - pyinstaller


The following packages will be downloaded:

    package                    |            build
    ---------------------------|-----------------
    altgraph-0.17              |             py_0          21 KB
    ca-certificates-2020.10.14 |                0         159 KB
    certifi-2020.6.20          |     pyhd3eb1b0_3         159 KB
    conda-4.9.2                |   py37haa95532_0         3.1 MB
    macholib-1.14              |             py_1          36 KB
    openssl-1.1.1c             |       he774522_1         5.7 MB
    pefile-2019.4.18           |             py_0          54 KB
    pycryptodome-3.7.3         |   py37he774522_0         5.9 MB
    pyinstaller-3.6            |   py37h62dcd97_5         2.4 MB
    pywin32-ctypes-0.2.0       |        py37_1001          38 KB
    ------------------------------------------------------------
                                           Total:        17.7 MB

The following NEW packages will be INSTALLED:

  altgraph           pkgs/main/noarch::altgraph-0.17-py_0
  macholib           pkgs/main/noarch::macholib-1.14-py_1
  pefile             pkgs/main/noarch::pefile-2019.4.18-py_0
  pycryptodome       pkgs/main/win-64::pycryptodome-3.7.3-py37he774522_0
  pyinstaller        pkgs/main/win-64::pyinstaller-3.6-py37h62dcd97_5
  pywin32-ctypes     pkgs/main/win-64::pywin32-ctypes-0.2.0-py37_1001

The following packages will be UPDATED:

  ca-certificates      anaconda::ca-certificates-2020.1.1-0 --> pkgs/main::ca-certificates-2020.10.14-0
  certifi            anaconda/win-64::certifi-2019.11.28-p~ --> pkgs/main/noarch::certifi-2020.6.20-pyhd3eb1b0_3
  conda                        anaconda::conda-4.8.3-py37_0 --> pkgs/main::conda-4.9.2-py37haa95532_0

The following packages will be SUPERSEDED by a higher-priority channel:

  openssl                anaconda::openssl-1.1.1-he774522_0 --> pkgs/main::openssl-1.1.1c-he774522_1


Proceed ([y]/n)?

 と、いうことでセットアップ完了です。あ、、、完了してませんでした。「y」を押して、エンターキーで続行します。

Proceed ([y]/n)? y


Downloading and Extracting Packages
macholib-1.14        | 36 KB     | ########################################### | 100%
pywin32-ctypes-0.2.0 | 38 KB     | ########################################### | 100%
ca-certificates-2020 | 159 KB    | ########################################### | 100%
pyinstaller-3.6      | 2.4 MB    | ########################################### | 100%
conda-4.9.2          | 3.1 MB    | ########################################### | 100%
openssl-1.1.1c       | 5.7 MB    | ########################################### | 100%
pycryptodome-3.7.3   | 5.9 MB    | ########################################### | 100%
pefile-2019.4.18     | 54 KB     | ########################################### | 100%
certifi-2020.6.20    | 159 KB    | ########################################### | 100%
altgraph-0.17        | 21 KB     | ########################################### | 100%
Preparing transaction: done
Verifying transaction: failed

EnvironmentNotWritableError: The current user does not have write permissions to the target environment.
  environment location: C:\ProgramData\Anaconda3



(base) C:\work>

 ということで今度こそ、完了、、、と、思いましが「EnvironmentNotWritableError」が出ていました。これは、Anaconda3を実行したユーザーに権限がなくて、書き込めていない、ということのようです。ということで、気を取り直して、Anaconda Promptを管理者権限で実行して、再度やり直しです。ダウンロードまでは済んでいたので、スキップされました。もう一度Proceed ([y]/n)? で「y」を入力すると、4~5行ほど何かが表示された後クリアされ、

done

(base) C:\work> 

と出力されました。「done」だから、完了、ですよね?

お試し

 では、さっそくお試しです。お試しコマンドは公式ホームページの下の方にありました。

pyinstaller yourprogram.py

 前回つくったスクリプト「connecter_test3.py」でお試ししてみました。実行結果は以下の通りですね。

(base) C:\work\tkinter_example>pyinstaller connecter_test3.py
61 INFO: PyInstaller: 3.6
62 INFO: Python: 3.7.3 (conda)
64 INFO: Platform: Windows-10-10.0.18362-SP0
65 INFO: wrote C:\work\tkinter_example\connecter_test3.spec
68 INFO: UPX is not available.
70 INFO: Extending PYTHONPATH with paths
['C:\\work\\tkinter_example', 'C:\\work\\tkinter_example']
70 INFO: checking Analysis
71 INFO: Building Analysis because Analysis-00.toc is non existent
72 INFO: Initializing module dependency graph...
84 INFO: Caching module graph hooks...
96 INFO: Analyzing base_library.zip ...
5632 INFO: Caching module dependency graph...
5744 INFO: running Analysis Analysis-00.toc
5769 INFO: Adding Microsoft.Windows.Common-Controls to dependent assemblies of final executable
  required by C:\ProgramData\Anaconda3\python.exe
6120 INFO: Analyzing C:\work\tkinter_example\connecter_test3.py
6308 INFO: Processing module hooks...
6309 INFO: Loading module hook "hook-encodings.py"...
6411 INFO: Loading module hook "hook-pydoc.py"...
6413 INFO: Loading module hook "hook-xml.py"...
6886 INFO: Loading module hook "hook-_tkinter.py"...
7253 INFO: checking Tree
7253 INFO: Building Tree because Tree-00.toc is non existent
7259 INFO: Building Tree Tree-00.toc
7355 INFO: checking Tree
7356 INFO: Building Tree because Tree-01.toc is non existent
7357 INFO: Building Tree Tree-01.toc
7396 INFO: Looking for ctypes DLLs
7418 INFO: Analyzing run-time hooks ...
7423 INFO: Including run-time hook 'pyi_rth__tkinter.py'
7429 INFO: Looking for dynamic libraries
7698 INFO: Looking for eggs
7699 INFO: Using Python library C:\ProgramData\Anaconda3\python37.dll
7700 INFO: Found binding redirects:
[]
7703 INFO: Warnings written to C:\work\tkinter_example\build\connecter_test3\warn-connecter_test3.txt
7742 INFO: Graph cross-reference written to C:\work\tkinter_example\build\connecter_test3\xref-connecter_test3.html
7784 INFO: checking PYZ
7784 INFO: Building PYZ because PYZ-00.toc is non existent
7786 INFO: Building PYZ (ZlibArchive) C:\work\tkinter_example\build\connecter_test3\PYZ-00.pyz
8370 INFO: Building PYZ (ZlibArchive) C:\work\tkinter_example\build\connecter_test3\PYZ-00.pyz completed successfully.
8378 INFO: checking PKG
8379 INFO: Building PKG because PKG-00.toc is non existent
8379 INFO: Building PKG (CArchive) PKG-00.pkg
8398 INFO: Building PKG (CArchive) PKG-00.pkg completed successfully.
8400 INFO: Bootloader C:\ProgramData\Anaconda3\lib\site-packages\PyInstaller\bootloader\Windows-64bit\run.exe
8400 INFO: checking EXE
8401 INFO: Building EXE because EXE-00.toc is non existent
8401 INFO: Building EXE from EXE-00.toc
8402 INFO: Appending archive to EXE C:\work\tkinter_example\build\connecter_test3\connecter_test3.exe
8406 INFO: Building EXE from EXE-00.toc completed successfully.
8410 INFO: checking COLLECT
8411 INFO: Building COLLECT because COLLECT-00.toc is non existent
8412 INFO: Building COLLECT COLLECT-00.toc
10094 INFO: Building COLLECT COLLECT-00.toc completed successfully.

(base) C:\work\tkinter_example>

 配下に「build」「dist」サブフォルダができていました。本家のページに書いてありましたが「dist」というサブディレクトリにバンドルが生成される、ということですので「dist」の方が最終的な出力先なのでしょう。両方のディレクトリに「connecter_test3.exe」が生成されていましたので、実行してみました。「dist」は動きましたが、「build」は動きませんでした。ただ、「dist」の動作結果も以下の通り、コマンドプロンプトのウィンドウが余分に表示されていましたので、GUIプログラムのバンドルを生成する場合は、オプションでなんとかしなければいけません。

distへできあがったconnecter_test3.exeをエクスプローラからダブルクリックで実行したところ。

 ちなみに、「build」の方ですが、コマンドプロンプトから実行して確認してみたところ、メッセージが出力されていました。

(base) C:\work\tkinter_example\build\connecter_test3>connecter_test3.exe
Error loading Python DLL 'C:\work\tkinter_example\build\connecter_test3\python37.dll'.
LoadLibrary: 指定されたモジュールが見つかりません。

(base) C:\work\tkinter_example\build\connecter_test3>

 「dist」のディレクトリには「python37.dll」がありましたので、「build」は途中生成物か、実行環境上での共通ではない変更されるもの、ということなのかな?

 そういえば、色々見ている中で、PyInstallerには、–onefileオプションがあったなぁ、ということで使い方を見てみました。で、出力される実行ファイルを一つに、コンソールなしで、というコマンドは以下の通りでした。

pyinstaller --onefile --windowed connecter_test3.py

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

(base) C:\work\tkinter_example>pyinstaller --onefile --windowed connecter_test3.py
79 INFO: PyInstaller: 3.6
80 INFO: Python: 3.7.3 (conda)
81 INFO: Platform: Windows-10-10.0.18362-SP0
82 INFO: wrote C:\work\tkinter_example\connecter_test3.spec
85 INFO: UPX is not available.
86 INFO: Extending PYTHONPATH with paths
['C:\\work\\tkinter_example', 'C:\\work\\tkinter_example']
86 INFO: checking Analysis
160 INFO: checking PYZ
182 INFO: checking PKG
235 INFO: Building because C:\work\tkinter_example\build\connecter_test3\connecter_test3.exe.manifest changed
235 INFO: Building PKG (CArchive) PKG-00.pkg
2640 INFO: Building PKG (CArchive) PKG-00.pkg completed successfully.
2663 INFO: Bootloader C:\ProgramData\Anaconda3\lib\site-packages\PyInstaller\bootloader\Windows-64bit\runw.exe
2663 INFO: checking EXE
2673 INFO: Building because console changed
2673 INFO: Building EXE from EXE-00.toc
2676 INFO: Appending archive to EXE C:\work\tkinter_example\dist\connecter_test3.exe
2686 INFO: Building EXE from EXE-00.toc completed successfully.

(base) C:\work\tkinter_example>

 実行したら、引数の.pyファイルを分析して、.specファイルができるとのこと。おお、確かに、できてました。

# -*- mode: python ; coding: utf-8 -*-

block_cipher = None


a = Analysis(['connecter_test3.py'],
             pathex=['C:\\work\\tkinter_example'],
             binaries=[],
             datas=[],
             hiddenimports=[],
             hookspath=[],
             runtime_hooks=[],
             excludes=[],
             win_no_prefer_redirects=False,
             win_private_assemblies=False,
             cipher=block_cipher,
             noarchive=False)
pyz = PYZ(a.pure, a.zipped_data,
             cipher=block_cipher)
exe = EXE(pyz,
          a.scripts,
          a.binaries,
          a.zipfiles,
          a.datas,
          [],
          name='connecter_test3',
          debug=False,
          bootloader_ignore_signals=False,
          strip=False,
          upx=True,
          upx_exclude=[],
          runtime_tmpdir=None,
          console=False )

 specファイルは、pyinstallerコマンドに指定して実行することができるファイルで、コマンドラインで指定したオプションのかわりになるファイルです。specファイルの書き方の詳細は、ここにありました。まずは、コマンドラインで実行してspecファイルを生成してから、以降はこのspecファイルで生成するのがよいでしょうか。

まとめ

 いろいろと実験してみましたが、アイコンファイルや画像ファイル等の別ファイルを使うとコマンドラインオプションでいろいろと指定する必要が出てきそうです。そんなときは、コマンドラインの指定をバッチスクリプトにしておくか、specファイルを利用する、というのが推奨されたやり方のようです。

Python tkinter GUIプログラミング Canvasで箱をつなぐ線を描くその3

 今日も見に来てくださって、ありがとうございます。石川さんです。

 さて、その3ですが、その1を書いたときに宣言した通り、リファクタリングをやります。pylintさんから、こんなリファクタリングをした方がいいんじゃない?、と、提案がありました。

************* Module connecter_test2
R0902: 11,0: Entity: Too many instance attributes (9/7)
R0913: 13,4: Entity.__init__: Too many arguments (6/5)

------------------------------------------------------------------

Your code has been rated at 9.75/10 (previous run: 9.75/10, +0.00)

 はい、Entityのインスタンスのアトリビュートが多すぎますよ、初期化のメソッドの引数が多すぎますよ、と、申しております。インスタンスのアトリビュートは上限7個が適当だとpylint設計者は考えているようですね。確かに、TMで分析してアトリビュートはだいたい5個未満にならないとおかしいもんね。たまにアトリビュートが多いヤツもありますけどねぇ。7個までというのは、直観的にもいい数字ですね。そして、引数の上限は5個、まあ、妥当でしょうね。あんまり引数が多すぎると、間違いのもとですからね。

 アトリビュートを見てみますと、canvas、x、y、width、height、id、start_x、start_y、connectionsと、、、まあまあ多いですね。Pointをつくるとx、yは一つになりますね。どうしましょうか。今回は箱をつくるということで、Rectangleを用意してみることにしました。そして、前からずっと使いたかった、namedtupleを使ってみることにしました。

ソースコード

 ソースコードは以下の通りです。

import tkinter as tk
from collections import namedtuple

Rectangle = namedtuple('Rectangle', ['x', 'y', 'width', 'height'],
                       defaults=(60, 40))


class Entity():
    ''' Entity class '''
    def __init__(self, canvas, rectangle):
        self.canvas = canvas
        self.rectangle = rectangle
        self.start_x = self.start_y = None
        self.connections = []

        self.id = self.canvas.create_rectangle(rectangle.x, rectangle.y,
                                               rectangle.x + rectangle.width,
                                               rectangle.y + rectangle.height,
                                               fill="lightblue", width=3)
        self.canvas.tag_bind(self.id, "<ButtonPress>", self.button_press)
        self.canvas.tag_bind(self.id, "<Motion>", self.move)
        self.canvas.tag_bind(self.id, "<ButtonRelease>", self.button_release)

    def button_press(self, event):
        ''' マウスのボタンが押されたときの処理 '''
        self.start_x = self.canvas.canvasx(event.x)
        self.start_y = self.canvas.canvasy(event.y)

    def move(self, event):
        ''' マウスが移動したときの処理 '''
        if event.state & 256:  # マウスボタン1が押されているときだけ(ドラッグ中のみ)
            can_x = self.canvas.canvasx(event.x)
            can_y = self.canvas.canvasy(event.y)
            coords = self.canvas.coords(self.id)
            coords[0] -= self.start_x - can_x
            coords[1] -= self.start_y - can_y
            coords[2] -= self.start_x - can_x
            coords[3] -= self.start_y - can_y
            self.canvas.coords(self.id, coords)
            self.start_x = can_x
            self.start_y = can_y
            self.rectangle = Rectangle(*coords[0:2], *self.rectangle[2:4])
            for connection in self.connections:
                connection.move(self)

    def button_release(self, event):  # pylint: disable=unused-argument
        ''' マウスのボタンが離されたとき '''
        self.start_x = self.start_y = None

    def get_center(self):
        ''' 中心座標を戻します '''
        center_x = self.rectangle.x + self.rectangle.width//2
        center_y = self.rectangle.y + self.rectangle.height//2
        return center_x, center_y

    def add_listener(self, connection):
        ''' コネクションのリスナーを登録します '''
        self.connections.append(connection)


class Connection():
    ''' Connection class '''
    def __init__(self, canvas, start_entity, end_entity):
        self.canvas = canvas
        self.start_e = start_entity
        self.end_e = end_entity
        start_entity.add_listener(self)
        end_entity.add_listener(self)

        self.id = self.canvas.create_line(self.get_intersection(start_entity),
                                          self.get_intersection(end_entity))

    def move(self, entity):
        ''' エンティティが移動したときの処理(エンティティから呼び出される)'''
        coords = self.canvas.coords(self.id)
        if entity == self.start_e:
            coords[0:2] = self.get_intersection(entity)
            coords[2:4] = self.get_intersection(self.end_e)
        elif entity == self.end_e:
            coords[0:2] = self.get_intersection(self.start_e)
            coords[2:4] = self.get_intersection(entity)
        self.canvas.coords(self.id, coords)

    def get_intersection(self, entity):
        ''' 矩形の接点を求める '''
        x, y = entity.get_center()
        height, width = entity.rectangle.height//2, entity.rectangle.width//2
        dx = self.end_e.rectangle.x - self.start_e.rectangle.x
        dy = self.end_e.rectangle.y - self.start_e.rectangle.y
        if entity == self.end_e:
            dx, dy = -dx, -dy
        if abs(dy / dx) < (height / width):  # 垂直側
            x_pos = x + width if dx > 0 else x - width
            y_pos = y + dy * width / abs(dx)
        else:  # 水平側
            x_pos = x + dx * height / abs(dy)
            y_pos = y + height if dy > 0 else y - height
        return x_pos, y_pos


class Application(tk.Tk):
    ''' Application class '''
    def __init__(self):
        super().__init__()
        self.title("Connecter test 3")
        self.geometry("640x320")

        self.canvas = tk.Canvas(self, background="white")
        self.canvas.pack(fill=tk.BOTH, expand=True)

        entity1 = Entity(self.canvas, Rectangle(40, 80))
        entity2 = Entity(self.canvas, Rectangle(240, 160))
        Connection(self.canvas, entity1, entity2)
        entity3 = Entity(self.canvas, Rectangle(420, 60))
        Connection(self.canvas, entity2, entity3)


def main():
    ''' main function '''
    application = Application()
    application.mainloop()


if __name__ == "__main__":
    main()

説明

 まずは、2行目です。namedtupleはcollectionsモジュールに含まれていますので、まずはimportしておきましょう。これでnamedtupleを使えるようになりました。4行目でさっそく使っています。これだけで、x、y、width、heightの4つのアトリビュートをもつRectangleクラスと同等のものができあがりました。defaultsオプションは後ろの方の初期値になります。

Rectangle = namedtuple('Rectangle', ['x', 'y', 'width', 'height'],
                       defaults=(60, 40))

 今回の場合は、widthとheightが省略された場合、これらに対し、60と40がセットされます。

 これで、Entityクラスの__init__()メソッドの4つのパラメータが一つにまとまりますので、これだけで「Too many arguments」のリファクタリングは完了です。あとは、インスタンスアトリビュートも地道に変更すれば完了です。self.xをself.retangle.x、self.yをself.rectangle.y、self.widthをself.rectangle.widht、self.heightをself.rectangle.heightといった具合にすべて変更しました。あと、111行目あたりのEntity作成時の初期値部分をRectangle(40, 80)というふうに変更しました。

 実行していたわかったことですが、namedtupleは名前がタプルということだけあって、不変なのですね。箱を動かしたときに、座標をセットしていたのですけど、self.rectangle.xは値を変えられませんでした。ま、名前から想像すれば、当たり前のことだったのですけどね。と、いうことで、唯一の座標を保持するための場所は、Rectangleクラスを再作成することにしました。42行目です。「Rectangle(*coords[0:2], *self.rectangle[2:4])」というふうに初期化しているのですが、namedtupleで生成したタプルは、属性が名称で参照できるだけでなく、タプルと同じように位置でアクセスすることもできるのですね。すばらしいです!

まとめ

 インスタンスアトリビュートの数は7個までにおさえましょう。属性をまとめるのには、namedtupleが便利です。