echo("備忘録");

IT技術やプログラミング関連など、技術系の事を備忘録的にまとめています。

【Excella】Excella Reportsで書式・スタイル設定を行う方法

現在、仕事で「Excella Reports」という、JavaからExcel/PDFを出力するライブラリを使用してます。

Excella Reports

便利なのですが、中の人(?)も言っている通り、書式やスタイル設定はちょっと弱いところのようです。

なので、分かる範囲で書式・スタイル設定についてまとめてみました

ソースコード

import org.bbreak.excella.core.SheetData;
import org.bbreak.excella.core.SheetParser;
import org.bbreak.excella.core.exception.ParseException;
import org.bbreak.excella.reports.listener.ReportProcessListener;
import org.bbreak.excella.reports.model.ReportBook;
import org.bbreak.excella.reports.model.ReportSheet;

public class Main {

    public static void main(String[] args) throws Exception {

        // テンプレートファイルの指定
        String templateFilePath = "SampleTemplate.xls";
        String outputFileName = "Sample";
        String outputFileDir = "Sample/Temp";
        String outputFilePath = outputFileDir.concat(outputFileName);

       // ReportBookの作成
        ReportBook outputBook = new ReportBook(templateFilePath,
                outputFilePath, ExcelExporter.FORMAT_TYPE);

        // シートの作成
        ReportSheet outputSheet = new ReportSheet("TemplateSheet", "Sheet1");
        outputBook.addReportSheet(outputSheet);

        // シートのセルに値を設定する。
        int data = 1.23;
        outputSheet.addParam(SingleParamParser.DEFAULT_TAG, "DATA1", data1);

        // ReportProcessListenerの定義&ブックの出力
        ReportProcessor reportProcessor = new ReportProcessor();
        reportProcessor.addReportProcessListener(new SheetFormatSetter());
        reportProcessor.process(outputBook);
    }
}
import org.apache.poi.ss.usermodel.Cell;
import org.apache.poi.ss.usermodel.Row;
import org.apache.poi.ss.usermodel.Font;
import org.apache.poi.ss.usermodel.DataFormat;
import org.apache.poi.ss.usermodel.CellStyle;
import org.apache.poi.ss.usermodel.Sheet;
import org.apache.poi.ss.usermodel.Workbook;
import org.bbreak.excella.core.SheetData;
import org.bbreak.excella.core.SheetParser;
import org.bbreak.excella.core.exception.ParseException;
import org.bbreak.excella.reports.listener.ReportProcessListener;
import org.bbreak.excella.reports.model.ReportBook;
import org.bbreak.excella.reports.model.ReportSheet;

public class SheetFormatSetterimplements ReportProcessListener {

    public SheetFormatSetter() {
    }

    @Override
    public void postParse(Sheet sheet, SheetParser sheetParser, SheetData sheetData) throws ParseException {
        
        // WorkbookはsheetのgetWorkBook()メソッドから取得。
        Workbook wb = sheet.getWorkBook();

        CellStyle style = wb.createCellStyle();

        // 表示形式、フォントの設定
        DataFormat format = wb.createDataFormat();
        style.setDataFormat(format.getFormat("#,##0.###");       
       
        Font font = wb.createFont();
        font.setFontName("MS 明朝");
        font.setFontHeightInPoints((short)10);
        style.setFont(font);

        // 縦横位置&上枠線の設定
        style.setAlignment(Cellstyle.ALIGN_RIGHT);
        style.setVerticalAlignment(Cellstyle.VERTICAL_CENTER); 
        style.setBorderTop(CellStyle.BORDER_THICK);
       
        // 対象セル(A1)を取得し、スタイルを設定
        Row row = sheet.getRow(0);
        Cell cell = row.getCell(0);

        cell.setCellStyle(style);
    
        return;
    }
}

基本的にはリファレンスにも書いてある通り、ReportProcessListenerのメソッド(preXX(), postXX())から、Apache POIのAPIを直接呼び出すことになります。

ブック&スタイルの作成

 Workbook wb = sheet.getWorkBook();
 CellStyle style = wb.createCellStyle();

Apache POIだと、CellStyleをHSSFWorkBookクラスのcreateCellStyle()メソッドで作成しますが、Excella ReportsのReportBookクラスには、それに該当するメソッドはありません。

ただし、ReportProcessListenerのメソッドの引数sheetのgetWorkBook()メソッドでWorkBookが取得できますので、そのcreateCellStyle()メソッドからCellStyleを取得できます。

スタイルの各書式の設定

// 上から順に、位置揃え(横)、位置揃え(縦)、セルの上枠線
style.setAlignment(Cellstyle.ALIGN_RIGHT);
style.setVerticalAlignment(Cellstyle.VERTICAL_CENTER); 
style.setBorderTop(CellStyle.BORDER_THICK);

スタイルの各プロパティの設定は、プロパティごとに設定用のメソッドが用意されているので、それを使用します。

上記のように、あらかじめ用意されている定数をそのまま使用する場合は、該当の定数を引数に指定します。

// 表示形式、およびフォント
DataFormat format = wb.createDataFormat();
style.setDataFormat(format.getFormat("#,##0.###");       
       
Font font = wb.createFont();
font.setFontName("MS 明朝");
font.setFontHeightInPoints((short)10);
style.setFont(font);

定数をそのまま使用するのではなく、自分で色々カスタマイズする場合、上記のようにWorkBookのcreateXX()メソッドで該当のインターフェースを取得後、

そのsetXX()メソッドで各種プロパティを設定し、最後にCellStyleの設定用メソッドの引数として該当のインターフェースのインスタンスを渡します。

実際にセルに設定する

Row row = sheet.getRow(0);
Cell cell = row.getCell(0);

cell.setCellStyle(style);

最後に、SheetのgetRow()メソッド、及びRowのgetCell()メソッドで対象のセルを取得し、そのsetCellStyle()メソッドの引数に先程のCellStyleのインスタンスを渡し、実際に書式・スタイルを適用します。

なお、長くなるので省略していますが、実際にはrowやcellのnullチェックを忘れないようにしてください。(値が設定されていない場合、nullが返ります。)

また、今回は値を決め打ちしたので不要でしたが、実際に表示形式など、値の内容に依存する書式を設定する際は、事前にCellのgetCellType()メソッドで、セルの値の型を調べるのを忘れないようにしてください。

参考サイト


前回の投稿から2ヶ月...確かに新年入ってから、すごくメンタルの調子が悪かったのです。

...でも、もう少し更新頻度は上げていきたいです。

愛知県知多半島に住むフリーランスエンジニアのお話

これは「IT地方エンジニア Advent Calendar 2018」 12/24日分の記事です。

知多半島」ってどこよ?

愛知県南西部、2つある半島のうち、「く」の字型の方の半島です。

f:id:Makky12:20181223192936p:plain
知多半島半田市

  • 夏はマリンレジャーや花火大会で盛り上がります。
  • 「知多娘」という萌えキャラで、半島一丸で町(?)おこしをしています。

知多娘。公式WEBサイト

また僕が住んでいるのは「半田市」という、知多半島の中心というべき市で、

  • 「蔵」や「山車」が有名。 また「ごんぎつね」の作者、新見南吉の出身地。
  • 山車が市内だけで31台もあり、5年に1回「はんだ山車まつり」という、市内31台の山車が一堂に集まる祭りがある。
  • 「酢」で有名な「ミツカン酢」の本社があるのが、この半田市

ただ実際に僕が住んでる区域は、半田市でもかなり西の場所なので、むしろ常滑焼や中部国際空港(セントレア)で有名な、常滑市のほうが近いんですけどね。(セントレアまで車で20分)

何やってる人なの?

そんな「都会」とは対極の場所に住んでいながら、(10年間のうつ病パニック障害との闘病を経て)現在はフリーランスエンジニアをしています。

主に業務用のWebシステム開発プロジェクトに参画しております。
それ以外にも、ECサイト開発、スマホゲームアプリ、デスクトップアプリ開発などもこなす、 割と「オールラウンダー」なフリーランスです。
またプログラミング以外にも、要件定義~運用まで、一通り全てこなせます。

言語としては

  • サーバー:Java(Springとか)、およびPHP(Laravel, Cake, Fuel)
  • クライアント:Javascript、TypeScript、Vue.js
  • その他:.net、WPF、Xamarin、Kotlin、swift

などが中心です。

あと、サーバー・クライアントなどに関係なく、C#大好き人間です。
それ以外にも、シェルスクリプト(bash)なんかも扱ったりします。

またOSは、WindowsLinuxが中心です。(次点がAndroid, Macあたり)
DBは、SQL ServerOracleMySQLPostgreSQLなんかを扱えます。

ちなみに、未経験の分野でも機会があれば、ガンガン挑戦していくタイプのエンジニアです。
(近方・遠方限らず、お仕事の連絡、お待ちしてます。リモートワーク可能だと、すごく嬉しいです。)

地方ならではの問題

中心地まで遠い

愛知県といえば、やはり大半の就業先は名古屋になります。
(一部三河地方にもありますが、少なくとも知多半島にはほぼ皆無です)

ドアツードアで30分」とか、そういう恵まれた環境なら良いですが、電車の乗車時間だけで40分、ドアツードアなら余裕で1時間オーバーです。

その結果、以下のような「時間の弊害」が発生します。

  • 定時終業でも、家に帰ったら結構遅い時間なので、なかなかスキルアップの時間(どころか、プライベートな時間すら)が取れない。
  • 勉強会や懇親会などに参加しても、生活サイクルや終電の関係で、不参加 or 早めに切り上げざるを得ないので、人脈形成や情報収集が十分にできない。

(昔ながらの)保守的な考えの企業(SI屋?)が多い

愛知県は、冗談で「ト〇タ県」と言われるほど、製造業(自動車関連)が盛んです。
自動車業界は、特にセキュリティには敏感な業界ですが、その影響か「リモートワーク不可」「フレックス勤務不可」の企業が多いです。 (もちろん例外もあります)

なにが嫌かって、「わざわざ生活時間帯を合わせなきゃいけない」点。

リモートワーク可能ならば全て解決なのですが、せめてフレックスぐらい可能にして、帰宅後の時間を増やしてくれ、と。
ただでさえ、通勤時間でかなり時間を無駄にしてますから。
少しでも時間を有効に使えるようにしたいです。

てか、TwitterのTLで流れてくる東京の情報を見ると、ちょっとうらやましくなります。

解決策

解決策ですが、下記のようなものでしょうか。

  • 東京の人とつながりを持ち、その人経由で案件をもらう
  • 時間を確保できる働き方ができる案件を、自分で(営業して)見つける。
  • 東京に移住する

東京の人とつながりを持つ

個人的に、興味のある技術の勉強会などで、わりと東京に行っています。(ほぼ品川か渋谷ですが...)

少しずつ活動はしていますが、まだまだ成果が出るのは先でしょうね...
そもそも、お金の関係もあり、ホイホイと気軽に行けるわけでもないですし。

それでも、今後もそういう活動を続けないと、と思います。
だって、単価も案件も一番魅力的なのは、やはり東京ですし。

自分で営業する

これが出来ると良いんですが。
てか自分でも「いずれは出来るようにならないと」と思ってます。

そのためにも、

  • 地元の人(技術者やIT業界の関係者)とつながりを持つ
  • ポートフォリオサイトなど、営業ツールを作成する。
  • いろんな会合に、積極的に参加し、人脈&交流を広める。

をしなくては、と思ってます。

そして「リモート可」「週4可能」のような、自分に時間のゆとりが持てる働き方を手に入れなければ、と思ってます。
そして、その時間を、さらに自分のスキルアップや営業力アップに充てられれば、いい正のスパイラルができるなあ、と。

てか友人の女性にも「行動しないと、仕事も奥さんも手に入らないよ!」と言われましたし...)

東京に移住する

これが出来れば、一番問題ないんでしょうけどね。

ただ残念ながら、最初に少し書いた「うつ病パニック障害」の関係で、移住(てか、それ以前に一人暮らしが)NGなのです...

※通常勤務自体は問題ないんですが、先述の病気の関係で、周囲の生活音の影響が大きい一人暮らしは、医師や相談医から「絶対NG!」と強く言われております。
そのため、未だに実家住まい(実家から出られない)です...

※てか、「東京に移住」が出来るなら、こんな長文ブログ、書く必要ないもんな...

最後に

来年以降、こんなことやりたいなあ、なんて思ってます。

  • 上に書いたことを、少しずつでよいから、続けていく
  • LT登壇
  • フリーエンジニアの人と、もっと交流を持って、いろんな情報を共有する(分野、地方問わず)
  • 懇親会にも出来るだけ参加して、いろいろな人と関わっていく
  • ITイベントの運営(特に地方の市町村で行われるような「地元密着型」イベント)
    ※今年「ブイアールサンダー」という、豊橋市二川で行われたイベントに参加させて頂いたのですが、あんな感じのイベントの運営に携わりたいなあ、と。

以上、ものすごい長文になってしまいましたが、最後まで読んでいただき、本当にありがとうございました。

それでは、よいクリスマスイブを!

明日12/25、大トリの日記は、taikiixさんです。

【2018/12/22】NGK2018Bに参加した感想

「NGK2018B」とは?

名古屋最大級のIT系イベント(LT発表大会)です。
今年は170名近くが参加されたようです。(僕も含め)


どんなLTがあったの?

詳細は上記リンク(昼の部)を参照。ただ

  • IT系
  • 非IT系
  • IT系イベント、勉強会告知

など、IT系・非IT系問わず、いろいろありました。(門戸が広い!)
なので、それを通して「色々な技術や分野があるんだなあ」と改めて知ることが出来ました。

感じたこと

  • そもそもLTできる人は、LTネタにできる技術に(業務・プライベート関わらず) 携わってる。
  • そして、どんな些細なな事でも「インプット→アウトプット」という流れを実践されている。
  • やはりやる人は、忙しい中でも時間を見つけて、スキルアップにつながる作業に割いていることをひしひしと実感。
  • 優秀な人は、みんな東京や海外に行っちゃうんだなあ。(当たり前か)

思ったこと

  • 「業務が終わると、帰ったら疲れて何もできない」なんて「言い訳」なのかも。
  • 少ない時間でも、そういう活動をする時間を作らないといけない
  • でか、それができるように就業スタイルを変えていかないといけない。(そういうやりかたこそ、勉強しなければならない)
  • そもそも、いろいろ行動して、色々な事に携わらなければ、いつまでたってもLTネタなんかできるわけがない。
  • てか、それってプライベートでも同じだよね?

と、反省しきりでした。

気になったこと

  • 「アンチ」や「ヘイト」ほどでもないですが、disり系?のLTが一部あった。
  • 僕はそこまで気にならなかったが、相当気になった人もいたらしく、実際タイムラインにも「〇〇がなければ××なのに…このTLだけはマジで不愉快!」みたいなツイートもあった。
  • やたらに縛る必要はないと思うが、ある程度の線引きは必要なのかも...ね。

夜の部(懇親会)について

  • 参加予定だったが、家庭の事情で参加できず...(いろいろ情報交換・情報収集したかったなあ。)
  • LT大会がメインだから仕方ないけど、規模が大きいから、どうしても開始&終了が遅くなってしまいますね。(だから帰りも遅くなってしまう。遠方の田舎民にはつらいっす)
  • そういうこともあり、「LT大会」と「懇親会」を分ける案も出ているそうですね。個人的にはいい考えだと思います。(開始&終了が早ければ、僕のような終電が近い田舎民にも安心♪)

で、結論

来年は、下記のどちらかを達成したい。

  • LT登壇
  • 運営側に回って、何か力になりたい。(できれば大きなものではなく、地元密着型のような、自分も愛着がある場所での勉強会&懇親会で)

とりあえず、そんな感じでした。

【Office】Office365 Businessを申し込む方法や注意点

経緯

  • 今までOffce365 &Office Personal Premiun&永続版Excel&Word(2013)で使用していたが、所持PCが多くなり、賄えなくなってきた。
  • Office365 Soloを検討したが、Office365 Businessのほうがメリットが多そうだった。(値段・機能など)
  • Office365 Businessは、個人事業主でもOKみたいだった。

上記のこともあり、今回の更新から、Office365 Businessに変えることにした。

公式サイト:Microsoft Office 全製品の比較 | Microsoft Office

SoloとBusinessの違い

細かいところは色々ありますが、大きなところでは、下記あたりでしょうか。

項目 Solo Business
値段 12,744円/年 11,664円/年
Skype ×
OneDrive OneDrive OneDrive For Business
インストール可能台数 無制限 PC×5、スマホ×5、タブレット×5
同時使用可能台数 5台まで(機種問わず) 制限なし
個人アカウントとの連携 ×
  • 公式サイトを見るとBusinessは「900円/月(10800円/年)」に見えるが、あれば「税抜」価格(Soloは「税込」価格)。税込にすると上記の通り、90円/月(1080円/年)しか違わないので、値段は決定的な違いにはならない(と思う)。
  • むしろ他の要素の方が重要。(例えば、Businessは個人アカウントとの連携不可なので、OneDriveのデータは自分で移行しなければならない、ということ。)
  • 最大の違いは「インストール可能台数」と「同時使用可能台数」。2018/10/2よりSoloは「インストール台数は無制限だが、同時使用は機種を問わず5台まで」になった。
  • 一方Businessは「インストール台数は有限だが、同時使用台数は無制限」となっている。(てか公式サイトの記載、あれじゃ全然分からない...)

単純に「値段が安いから」で選ぶと、かえって面倒なことになったりするので、よく考えて決めましょう。

登録方法

下記のページを参照に、登録を行った。

注意点としては、下記の所でしょうか。 (またスクショなくて、申し訳ないです...)

■手順1(本人情報入力)

  • 「電子メール」は、個人アカウントのものと同一ではダメかも(「事業用」のものが必要?ちゃんと分けるのが無難だと思う。)
  • そのほかの情報(住所、電話番号など)は個人アカウントと同じで問題なかった。(携帯番号でもOK)

■手順2(ユーザーID作成)

  • ユーザーIDは、「【個人ID】@【会社ID】.onmicrosoft.com」 で固定(例えば、makky12@hatenablog.onmicrosoft.comなど)。
  • 【個人ID】:メアドの@の前の部分、【会社ID】:メアドのドメイン部分、と考えればいいかも。
  • 右上に「既存のサブスクリプションに追加しますか?」と表示されているが、個人のアカウントには紐づけ不可なので注意(法人用の既存アカウントのみ)。

■手順3(注文の確認)&手順4(注文)

  • 人数を聞かれるが、個人利用なら「1人」で全く問題ない。
  • マイクロソフトのOffice365 Businessページだと月払いしかできない感じだが、年払いも選択可能。
  • (参考ページにも書いてあるけど)ここでOffice365 Businessページの値段が「税抜」価格であるとわかる。(そう書いといてよ...と思う、確かに)

登録完了後

あとは、先述の「【個人ID】@【会社ID】.onmicrosoft.com」で専用ページにログイン後、インストーラをダウンロードして、各種OfficeソフトをインストールすればOK。(インストール可能なソフトはSolo/Businessで違いはない。)

Businessの場合の作業

  • 個人アカウントと連携不可なので、OneDrive内のファイルは、期限終了前に自分でOneDrive For Businessに移行しないといけない。(移行ツールなどは存在しない)
  • (僕のケースのように)永続ライセンスのOfficeソフト等を使っている場合、(必要ないのに)変にアンインストールなどをしないほうが良いと思う。(やっぱり再インストール...と思った場合、ライセンス認証がクッソ面倒。なぜか「台数が上限に」とか「電話での認証不可」とか言われたりするし)

単に値段だけで決めるのは早計ですが、やっぱり使用可能台数の差は大きいです。よく考えて、自分に合ったライセンスを選びましょう。

でも使用可能台数が多いのは、やはり便利です。これからSurfaceなど、別PCを買う機会も増えるでしょうし

【Xamarin】ペアリング済のBluetoothデバイスの情報を取得する。

やりたいこと

  • (今も愛用中)のガラケー購入時にBluetoothスピーカーをもらったが、リモコンがなく、音量調整が面倒。

  • iTunesは音量調整出来るが、微妙な音量調節が難しい。(特に手が大きいと)

  • Androidでできるアプリを持ってない。→ならXamarinで作ってみようか。※出来るとは言ってない

ハマった?事

  • 最初は、下記の「Bluetooth LE Plugin for Xamarin」を使おうとしたが、「ペアリング済の機器情報」を取得する方法が分からなかった。(あったならすいません...)

  • Bluetoothの「UUID」は「機器ごと」に個別ではなく「サービス/プロファイル」ごとに個別だった。(同じ「スピーカー」ならUUIDは同じだが、それを知らずにハマった…いい勉強になりました。)

参考にした情報

ソース&結果

※すいません。時間の関係で、ソース&結果のみ表示します。
(詳細は後日追記します。)

■SearchDevicePage.xaml(VolChanger.View)

<?xml version="1.0" encoding="utf-8" ?>
<ContentPage xmlns="http://xamarin.com/schemas/2014/forms"
             xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
             x:Class="VolChanger.View.SearchDevicePage">
    <ContentPage.Content>
        <StackLayout>
            <ActivityIndicator x:Name="AILoading" IsVisible="{Binding IsSearching}" IsRunning="{Binding IsSearching}" />
            <Button x:Name="ButtonSearch" Text="Bluetoothデバイスを検出する" Command="{Binding GetDevicesCommand}" />
            <ListView x:Name="ListViewDevices" ItemsSource="{Binding DeviceList}">

                <!--ここに ItemTemplate を追加-->
                <ListView.ItemTemplate>
                    <DataTemplate>
                        <TextCell Text="{Binding Name}" Detail="{Binding Detail}"/>
                    </DataTemplate>
                </ListView.ItemTemplate>
            </ListView>
        </StackLayout>
    </ContentPage.Content>
</ContentPage>

■SearchDevicePage.xaml.cs(VolChanger.View)

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;

using Xamarin.Forms;
using Xamarin.Forms.Xaml;
using VolChanger.View;
using VolChanger.ViewModel;

using Plugin.BLE.Abstractions;
using Plugin.BLE.Abstractions.Contracts;
using Plugin.BLE.Abstractions.Exceptions;

// using Reactive.Bindings;
// using Reactive.Bindings.Extensions;

namespace VolChanger.View
{
    [XamlCompilation(XamlCompilationOptions.Compile)]
    public partial class SearchDevicePage : ContentPage
    {
        private readonly SearchDeviceViewModel vm;

        public SearchDevicePage ()
        {
            InitializeComponent ();
            this.vm = new SearchDeviceViewModel();
            BindingContext = vm;

            // ButtonSearch.Clicked += ButtonSearch_Clicked;
        }
    }
}

■SearchDeviceViewModel.cs(VolChanger.ViewModel)

using System;
using System.Collections.Generic;
using System.Text;
using System.Collections.ObjectModel;
using System.ComponentModel;
using System.Diagnostics;
using System.Net.Http;
using System.Runtime.CompilerServices;
using System.Threading.Tasks;

using VolChanger.View;
using VolChanger.ViewModel;
using VolChanger.Model;

using Plugin.BLE.Abstractions;
using Plugin.BLE.Abstractions.Contracts;
using Plugin.BLE.Abstractions.Exceptions;
using Xamarin.Forms;
using Plugin.BLE.Abstractions.EventArgs;

namespace VolChanger.ViewModel
{
    public class SearchDeviceViewModel : INotifyPropertyChanged
    {
        public ObservableCollection<BTDevice> DeviceList { get; set; } 
        public Command GetDevicesCommand { get; }
        public event PropertyChangedEventHandler PropertyChanged;
        private static PropertyChangedEventArgs SearchPropertyChangedEventArgs = new PropertyChangedEventArgs(nameof(IsSearching));

        bool isSearching = false;
        public bool IsSearching
        {
            get { return isSearching; }
            set
            {
                if(isSearching == value) { return; }

                isSearching = value;
                OnPropertyChanged(nameof(IsSearching));

                // CanExecute (このコマンドが実行可能かどうか)を更新
                GetDevicesCommand.ChangeCanExecute();
            }
        }

        public SearchDeviceViewModel()
        {
            DeviceList = new ObservableCollection<BTDevice>();

            GetDevicesCommand = new Command(
                () => GetDevices(), // 実行する処理
                () => !IsSearching); // このコマンドが実行可能の時にtrueを返す
        }

        private void GetDevices()
        {
            if (IsSearching)
                return;
            
            try
            {
                IsSearching = true;

                DeviceList.Clear();
                
                IBTManager manager = DependencyService.Get<IBTManager>();
                var list = manager.GetBondedDevices();

                // await Adapter.StartScanningForDevicesAsync();
                if(list != null && list.Count > 0)
                {
                    foreach(var dev in list)
                    {
                        DeviceList.Add(dev);
                    }
                }
                else
                { 
                    Application.Current.MainPage.DisplayAlert("VolChanger", "ペアリング済デバイスが見つかりませんでした。", "OK");
                }
            }
            catch (Exception e)
            {
                Application.Current.MainPage.DisplayAlert("Error!", e.Message, "OK");
            }
            finally
            {
                IsSearching = false;
            }
        }

        void OnPropertyChanged([CallerMemberName] string name = null) =>
            PropertyChanged?.Invoke(this, SearchPropertyChangedEventArgs);
    }
}

■BTDevice.cs(VolChanger.Model)

using System;
using System.Collections.Generic;
using System.Text;

namespace VolChanger.Model
{
    public class BTDevice
    {
        public string Name { get; set; }
        public string Uuid { get; set; }
        public string Adress { get; set; }
        public string Detail { get; set; }
    }
}

■IBTManager.cs(VolChanger.Model)

using System;
using System.Collections.Generic;
using System.Text;

using VolChanger.Model;

namespace VolChanger.Model
{
    public interface IBTManager
    {
        List<BTDevice> GetBondedDevices();
    }
}

■BTManager.cs(VolChanger.Android)

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;

using Android.App;
using Android.Content;
using Android.OS;
using Android.Runtime;
using Android.Views;
using Android.Widget;

using VolChanger.Model;
using Android.Bluetooth;

using Xamarin.Forms;

[assembly: Dependency(typeof(VolChanger.Droid.BTManager))]

namespace VolChanger.Droid
{
    public class BTManager: IBTManager
    {
        
        public List<BTDevice> GetBondedDevices()
        {
            // アダプター作成
            BluetoothAdapter adapter = BluetoothAdapter.DefaultAdapter; 

             // ペアリング済みデバイスの取得
            var listBondedDevices = adapter.BondedDevices;              
            var result = new List<BTDevice>();

            if (listBondedDevices != null && listBondedDevices.Count > 0)
            {
                foreach (var device in listBondedDevices)
                {
                    var btDevice = new BTDevice();
                    btDevice.Uuid = device.GetUuids().FirstOrDefault().ToString();

                    // Bluetoothスピーカーのみ取得する。
                    if (btDevice.Uuid.Substring(4, 4).ToUpper() == "110A" ||
                        btDevice.Uuid.Substring(4, 4).ToUpper() == "110B")
                    {
                        btDevice.Name = device.Name;
                        btDevice.Adress = device.Address;
                        btDevice.Detail = string.Format("MACアドレス:{0}", btDevice.Adress);
                        result.Add(btDevice);
                    }
                }
            }
            return result;
        }
    }
}

f:id:Makky12:20181201191328j:plain
結果(MACアドレスは一部編集で消してます)

あとは「音量の調整」なんですが、ここからが本当に難しいところでしょうね。

...まあ、それは旅行から帰ってきてからの続き、ということで。

【Linux】Ubuntu Mateを新規にインストールする

経緯

現在、Windows7のLet's Noteを持っているんですが、

panasonic.jp

  • Windows7のサポート終了(2019年1月)まで、あと約1年しかない。
  • 32bitマシンだけど、Windows10をインストールしても大丈夫?
  • それ以前に、まずWindows10がまともに動作するかも怪しい...

ということで、思い切って以前から用意しようと思っていた、Linuxをインストールすることにしました。

なぜUbuntuではないのか?

  • Ubuntu公式サイトに、現在のLTS最新版(18.04)の32bit版isoファイルがない。(64bitのみ。ただしインストール自体は32bitでも可能。)
  • Ubuntu公式が次のバージョンから、正式に32bitはサポートしない模様。

なので、ここはUbuntuではなく、32bit版isoファイルのDLが可能な、フレーバーをインストールすることに。

Ubuntu Mateを選んだ理由

  • カスタマイズ性が良さそうだった
  • 安定性・高速性、両方でバランスよさそうだった。
  • 要求スペックが低かった。(重要)

インストール方法

公式サイトから、*.isoファイルを入手する。
※torrent/直接DL、両方の形式がある。どちらでもOK。

ubuntu-mate.org

*.isoファイルをDVD・USBなどに焼く。(今はもう「焼く」って言わないのかも...)

※DVDでもUSBでも「起動ディスク」として利用できるならどちらでもOK。(僕は対象PCにDVDドライブがなかったので、USBにしました。)
※USBへのイメージファイル書き込みソフトは、「USB ISO 書き込み」等でググればたくさん出てくるので、お好きなものを。
BIOSUEFIなどの起動設定を変えるのを忘れないように。

インストール

てか、スクショ取り忘れたので、画像がない... 下記ページが参考になります。

  • 先述のDVD・USBをDVDドライブやUSBポートに入れた状態で、PCを起動すれば、インストーラが起動する。
  • 完全インストールならば「Install Ubuntu Mate」を選択する。
    ※「Try」の方は試用版の実行で、インストールはしません。とりあえずお試しの場合はこちらを。
  • Ubuntu Mateのインストール準備」画面では、「インストール中にアップデートを...」と「サードパーティー製ソフトのインストール」を両方チェックすると、面倒くさくない(かも)
  • 「インストールの種類」では、
    デュアルブートやら仮想環境のように、「既存OSを残す」場合は「既存のOSを残し、...」を選択する。
    クリーンインストールする場合は「ディスクを削除して...」を選択。(当然、前のOS&データはすべて削除されるので、ここだけは注意!)
  • 他にも設定項目があるが、あとは「言語選択」「地域選択」「ユーザー情報」等、特に問題ない項目なので割愛。

インストール完了したら

といっても、インストール完了したら、あとは再起動後にユーザー名&パスワードを入力すれば、起動するはず。

とりあえず、あとはお好みで...なのだが、デスクトップレイアウトだけでも、お好みで変えた方がいいかも。

デスクトップレイアウトは、「設定」-「MATE Tweak」から変更可能。 色々ありますが、下記2つあたり押さえておけばWin/Mac、どちらのユーザーでもOKかな?

  • Cupetino:Macっぽいドック&トップパネルがあるレイアウト。
  • RedMond:Windowsっぽい、左下に検索可能なメニューがある。

参考:

f:id:Makky12:20181124192554j:plain

以上、簡単ですが、紹介までに。

Micorosoft Cognitive Serviceの「Face API」をXamarin.Formsで使用する

概要

昨日(2018/11/17)、「初心者向けXamarinハンズオン」に参加したのですが、そこMicrosoft社の「Azure Face API」(顔検出・認識・分析API)を試してきたので、メモ。

イベントページ:https://jxug.connpass.com/event/96679/

※すでに終了してます。

使用方法

公式サイトから、下記いずれかのアカウントを取得する。

※この記事の内容は、「7日間試用版」で実施。

公式サイト:https://azure.microsoft.com/ja-jp/services/cognitive-services/face/

コーディング

公式サイトの左側ツリーの「クイックスタート」内に記載されている、

上記サンプルコードのうち、該当する方を参考にコーディングする。

※本記事は「クライアントSDK使用」にて実施。

サンプルソース

下記を参照。

※下記ソースは、「初心者向けXamarinハンズオン」の下記「本家リポジトリ」の

  • HandsOnLab/Start/DevDaysSpeakers/DevDaysSpeakers.Shared/View/DetailsPage.xaml.cs

の内容を、「手順書」の内容を基に編集したものです。

本家リポジトリhttps://github.com/jxug/dev-days-labs-ja/tree/master/hands-on

手順書:https://github.com/jxug/dev-days-labs-ja/tree/master/hands-on

※コメントが記載してない箇所は、「Xamarinハンズオン」に関係しますが「Face API」には直接関係ない処理です。

using System;
using DevDaysSpeakers.Model;
using Xamarin.Essentials;
using Xamarin.Forms;

// Face API関連のインポート
using Microsoft.Azure.CognitiveServices.Vision.Face;
using Microsoft.Azure.CognitiveServices.Vision.Face.Models;

using System.Threading.Tasks;
using System.Collections.Generic;

namespace DevDaysSpeakers.View
{
    public partial class DetailsPage : ContentPage
    {
        // エントリポイント
        private const string faceEndpoint =
            "https://westcentralus.api.cognitive.microsoft.com";

        // 取得したキー(キー1またはキー2)
        private const string subscriptionKey = "(Input Your Key)";

        // 取得したい(顔の)情報を配列で設定。
        private static readonly FaceAttributeType[] faceAttributes =
            { FaceAttributeType.Age, FaceAttributeType.Gender, FaceAttributeType.Emotion };

        readonly Speaker speaker;

        public DetailsPage(Speaker speaker)
        {
            InitializeComponent();

            SetActivityIndicatorProperty(false);

            this.speaker = speaker;
            BindingContext = this.speaker;

            ButtonSpeak.Clicked += ButtonSpeak_Clicked;
            ButtonWebsite.Clicked += ButtonWebsite_Clicked;
            ButtonDetectFace.Clicked += ButtonDetectFace_Clicked;
        }

        private async void ButtonSpeak_Clicked(object sender, EventArgs e)
        {
            await TextToSpeech.SpeakAsync(this.speaker.Description);
        }

        private void ButtonWebsite_Clicked(object sender, EventArgs e)
        {
            if (speaker.Website.StartsWith("http"))
                Device.OpenUri(new Uri(speaker.Website));
        }

        // 「顔を検出」ボタンクリック処理
        private async void ButtonDetectFace_Clicked(object sender, EventArgs e)
        {
            // FaceClientクラスの設定
            var faceClient = new FaceClient(
                new ApiKeyServiceClientCredentials(subscriptionKey),
                new System.Net.Http.DelegatingHandler[] { });
            faceClient.Endpoint = faceEndpoint;

            // インターネット上の顔画像ファイルのパス。(speaker.Avaterにパス情報が格納されています。)
            var remoteImageUrl = speaker.Avatar;

            // ローカル上の顔画像ファイルのパス
            // var localImagePath = @"<LocalImage>";

            try
            {
                SetActivityIndicatorProperty(true);

                // リモートの顔画像から情報を取得。
                // (ローカルの場合は「DetectWithStreamAsync()」メソッドを使用。)
                IList<DetectedFace> faceList = await faceClient.Face.DetectWithUrlAsync(
                    remoteImageUrl, true, false, faceAttributes);

                // 取得できなかった場合
                if (faceList.Count == 0 || faceList == null)
                    return;

                SetActivityIndicatorProperty(false);

                // 取得できた場合、そこから情報(年齢・性別・感情(幸福度))を取得し、メッセージボックスを表示。
                var age = faceList[0].FaceAttributes.Age;
                var gender = faceList[0].FaceAttributes.Gender;
                var emotion = (faceList[0].FaceAttributes.Emotion.Happiness * 100.000).ToString();

                var message = string.Format("年齢:{0}、性別:{1}、幸福度:{2} %", age, gender, emotion);
                await DisplayAlert("FaceAPI", message, "OK");
            }
            catch (APIErrorException exApi)
            {
                throw new Exception(exApi.Message.ToString());
            }
            catch (Exception ex)
            {
                await DisplayAlert("FaceAPI Exception", ex.ToString(), "OK");
            }
            finally
            {
                SetActivityIndicatorProperty(false);
            }
        }

        private void SetActivityIndicatorProperty(bool value)
        {
            AILoading.IsRunning = value;
            AILoading.IsVisible = value;
        }
    }
}

ポイントとしては、

※下記のインポート(using)

※取得したい(顔の)情報(FaceAttributeType)をFaceAttributeType配列で設定。

FaceAtteributeについては、下記ページが参考になります。

参考ページ:Microsofrt AzureのFace APIを使って笑顔をAIに判断してもらおう

※エントリポイント、キーの設定

※リモート/ローカル画像に応じたメソッド実施(DetectWithUrlAsync/DetectWithStreamAsync)

でしょうか。

ハマった点

  • Azureアカウントの場合、EntryPointのURLは公式ソースと違い「国・地域」を反映させたものじゃないとダメ。 (7日間試用版の場合、公式ソース同様「westcentralus」で問題ないです。)

  • 発行されるEntryPointのURLの末尾に「Face/(バージョン)」と書かれているが、実際には「Face/(バージョン)」は不要。(ドメイン名まででよい。てか、公式ソースがそうなってるし...ちゃんと読めよ...)

実行結果

f:id:Makky12:20181118185932j:plain

てか、Face API(Cognitive Service)に限らず、Azureとか、どんどん触れていきたいと思いました。