- 投稿日:2020-05-19T23:55:03+09:00
UniTask2(RC版)まとめ
UniTask2
UniTask
が大きくアップデートされて、バージョン2系がRCとなりました。
今回はいち早くその変更点をまとめていきます。※
Ver 2.0.7-rc4
準拠で書いています。変更があり次第また直します。破壊的変更
まずは1系からの破壊的変更。導入時に気をつける必要があるところです。
名前空間の変更
名前空間が
UniRx.Async
からCysharp.Threading.Tasks
に変更されました。
もともとUniRx
のライブラリの一部だった名残なのですが、今回のアップデートで完全に名残がなくなりました。UniTaskのawait2度漬け禁止
同じ
UniTask
に対しての2回以上のawait
が明確に禁止となりました。
IValueTaskSource
準拠の挙動なので、これは本家ValueTask
と同じ挙動です。UniTaskの2回以上のawait禁止private async UniTaskVoid DoAsync(CancellationToken token) { try { var uniTask = GetAsync("https://unity.com/ja", token); // 1回目のawaitは問題ない await uniTask; // 同じオブジェクトに対して2回以上のawaitはできない // (InvalidOperationExceptionが発行される) await uniTask; } catch (InvalidOperationException e) { Debug.LogException(e); } } private async UniTask<string> GetAsync(string uri, CancellationToken token) { var uwr = UnityWebRequest.Get(uri); await uwr.SendWebRequest().ConfigureAwait(cancellation: token); return uwr.downloadHandler.text; }Preserve()
もし同じ
UniTask
を2回以上await
する必要があるならば、Preserve()
を利用しましょう。Preserve()private async UniTaskVoid DoAsync(CancellationToken token) { try { var uniTask = GetAsync("https://unity.com/ja", token); // Preserve()で何回でもawait可能なUniTaskに変換 var reusable = uniTask.Preserve(); await reusable; await reusable; } catch (InvalidOperationException e) { Debug.LogException(e); } }UniTask.Lazyの戻り値の変更
上記の多重
await
禁止の変更をうけて、UniTask.Lazy
の戻り値がAsyncLazy
に変換されました。public static AsyncLazy Lazy(Func<UniTask> factory) public static AsyncLazy<T> Lazy<T>(Func<UniTask<T>> factory)使い勝手としてはあまり変わってはいません。
UniTask.WhenAnyの戻り値変更
WhenAny
の戻り値も変更されました。旧public static async UniTask<(int winArgumentIndex, (bool hasResult, T0 result0), (bool hasResult, T1 result1), (bool asResult, T2 result2))> WhenAny<T0, T1, T2>()新public static UniTask<(int winArgumentIndex, T1 result1, T2 result2, T3 result3)> WhenAny<T1, T2, T3>()UniTask.DelayFrameの戻り値変更
UniTask.DelayFrame
の戻り値がUniTask<int>
からUniTask
に変更されました。既存機能への追加機能とか
全体的なパフォーマンスの向上
全体的にパフォーマンスが向上しています。
内部でUniTask
とAsyncMethodBuilder
、Runner
の再利用が自動的に行われるようになり、ゼロアロケーションで動作するようになりました。UniTaskCompletionSourceが再利用可能に
UniTaskCompletionSource
にReset()
メソッドが追加されました。
こちらを利用することでUniTaskCompletionSource
を再利用することができるようになりました。AutoResetUniTaskCompletionSource追加
AutoResetUniTaskCompletionSource
も追加されました。
こちらはUniTaskCompletionSource
と挙動は同じですが、破棄したときに内部のインスタンスが自動的に再利用される仕組みになっています。
UniTaskCompletionSource
との使い分けですが、何度も同じインスタンスを再利用するならUniTaskCompletionSource
を使う。
使い捨てで都度新しいものを使うならAutoResetUniTaskCompletionSource
を使う。
という使い分けをするとよいでしょう。ファクトリメソッドが追加
ファクトリメソッドがいくつか追加されました。
- UniTask.Create
- UniTask.Defer
- UniTask.VoidAction
- UniTask.VoidUnityAction
- UniTask.WaitUntilCanceled
PlayerLoopTiming追加
PlayerLoopTiming
が追加され、次の種類となりました。
- Initialization
- LastInitialization
- EarlyUpdate
- LastEarlyUpdate
- FixedUpdate
- LastFixedUpdate
- PreUpdate
- LastPreUpdate
- Update
- LastUpdate
- PreLateUpdate
- LastPreLateUpdate
- PostLateUpdate
- LastPostLateUpdate
とくにLastPostLateUpdateが重要で、こちらはコルーチンにおける
WaitForEndOfFrame
に相当します。
いままでUniTask
ではyield return new WaitForEndOfFrame()
ができなかったのですが、今回のアップデートから可能となりました。JobHandle.WaitAsync
JobSystem
のJobHandle
にWaitAsync
が追加されました。
こちらを利用することで、任意のPlayerLoop
のタイミングに切り替えてから完了待ちができます。// WaitForEndOfFrame相当の待機をしてからComplete() await jobHandle.WaitAsync(PlayerLoopTiming.LastPostLateUpdate);AsyncTriggerメソッドのIUniTaskAsyncEnumerable対応
AsyncTrigger
系のメソッド(this.GetAsyncCollisionEnterTrigger
とか)がIUniTaskAsyncEnumerable
に対応しました。新機能
UniTaskAsyncEnumerable/IUniTaskAsyncEnumerable
UniTaskAsyncEnumerable
はUniTask2
の目玉機能です。
IUniTaskAsyncEnumerable<T>
はC# 8.0
のIAsyncEnumerable<T>
をUniTask
として実装したものです。
なんとこちらはC# 7.x系のUnityでも利用可能になっています。何をするためのものかというと、「非同期処理を複数個まとめて扱う」ことができるようになる機能です。
Observable
(IObservable<T>
)と似ていますが、こちらはPull
型として機能するという違いがあります。Observableとの使い分け
Observable
はPush
型なのに対して、UniTaskAsyncEnumerable
はPull
型です。
そのためUniTaskAsyncEnumerable
では非同期処理の実行タイミングを受信側でコントロールできるというメリットがあります。UniTaskAsyncEnumerableで複数の非同期処理を扱うprivate async void Start() { var token = this.GetCancellationTokenOnDestroy(); var uris = new[] { "https://www.google.com/", "https://unity.com/ja", "https://github.com/" }; // URIのリストに対してアクセスしてデータをとってくる // ただし常に実行される通信は同時に1つであり、 // 前回のものが完了しないと次の通信に進まない await uris.ToUniTaskAsyncEnumerable() // 通信完了を待機してメッセージを発行する .SelectAwait(async x => await FetchAsync(x)) .ForEachAsync(x => Debug.Log(x), token); } private async UniTask<string> FetchAsync(string uri) { using (var uwr = UnityWebRequest.Get(uri)) { await uwr.SendWebRequest(); if (uwr.isNetworkError || uwr.isHttpError) { throw new Exception($"Error>{uwr.error}"); } return uwr.downloadHandler.text; } }対する
Observable
はPush
型のため、多数のObserver
に対してメッセージをブロードキャストするのに向いています。複数個の非同期処理を管理する場合は基本には
UniTaskAsyncEnumerable
を使う。
イベント駆動を制御する場合にはObservable
を使う。という使い方をするとよいでしょう。
ForEachAsync/ForEachAwaitAsync
C# 8.0
であればawait foreach
が使えるのですが、それが使えないUnityバージョンでは代替としてForEachAsync
を利用します。
感覚としては、IObservable<T>
に対するSubscribe()
に似ています。
ですがこちらは「ForEachAsync()
の完了をさらにawait
で待つ」といったことが可能となります。ForEachAsyncprivate async void Start() { var token = this.GetCancellationTokenOnDestroy(); // EveryUpdate()は毎フレームのタイミングで完了するUniTaskを返す await UniTaskAsyncEnumerable.EveryUpdate() .Select((_, x) => x) // 5回まで実行する .Take(5) // ForEachAsyncで待機する .ForEachAsync(async _ => Debug.Log(Time.frameCount), token); Debug.Log("Done!"); }また、
ForEachAsync
の他にForEachAwaitAsync
もあります。
こちらはデリゲートの内部でasync/await
が利用でき、この非同期処理が完了するまで次のメッセージを取りに行きません。ForEachAwaitAsyncawait UniTaskAsyncEnumerable.EveryUpdate() .Select((_, x) => x) // 5回まで実行する .Take(5) // ForEachAwaitAsyncで待機する .ForEachAwaitAsync(async _ => { // 10フレーム待ってから次のメッセージを取りに行く await UniTask.DelayFrame(10, cancellationToken: token); }, token);注意:
ForEachAsync
でasync/await
する
ミスしそうな点として、ForEachAsync
でasync/await
を書いてしまうことです。
こちらの場合、コンパイルエラーとならずに動作してしまうのにForEachAwaitAsyncと動作がまったく異なってしまいます。
UniTask2 RC4
でこちらの挙動はコンパイルエラーとなるようになり、安全になりました。もし
ForEachAsync
を使いつつ、UniTaskVoidなasync/await
を使いたい(非同期処理だけどその結果待ちをせずに次のMoveNextAsync()
を呼んで欲しい)場合はUniTask.Void
を併用しましょう。private async void Start() { var token = this.GetCancellationTokenOnDestroy(); await UniTaskAsyncEnumerable.EveryUpdate() .Select((_, x) => x) .Take(5) // 非同期処理を登録できるがこのasync/awaitの結果を // 待たずにForEachAsyncはすぐ次のメッセージを取りに行く .ForEachAsync(_ => UniTask.VoidAction(async () => { Debug.Log("before await:" + Time.frameCount); // ForEachAwaitAsyncはここのawaitを待つ await UniTask.DelayFrame(10, cancellationToken: token); Debug.Log("after await:" + Time.frameCount); }), token); Debug.Log("Done!"); }UniTaskAsyncEnumerableの作り方
UniTaskAsyncEnumerable
はさまざまな方法で生成することができます。ファクトリメソッド
- Return
- Repeat
- Empty
- Throw
- Never
- Range
- EveryUpdate
- Timer
- TimerFrame
- Interval
- IntervalFrame
他のデータ構造から変換
IEnumerable<T>
IObservable<T>
Task<T>
UniTaskT>
uGUIコンポーネントから変換
uGUIイベントからの変換public class FromUGui : MonoBehaviour { [SerializeField] private Button _button; private void Start() { var token = this.GetCancellationTokenOnDestroy(); // 連打防止ボタン // 1回ボタンを押したら2秒間無反応になる _button.OnClickAsAsyncEnumerable() .ForEachAwaitWithCancellationAsync(async (_, ct) => { Debug.Log("Clicked!"); await UniTask.Delay(2000, cancellationToken: ct); }, token); } }AsyncTriggerを利用する
this.GetAsyncCollisionEnterTrigger()
など。Channel
Channel
はGo
におけるChannel
と同義です。
挙動としてはObservable
におけるSubject
に相当します。なお、
Channel
は内部でメッセージをキューイングします。Channelの例private void Start() { // Channel作成 var channel = Channel.CreateSingleConsumerUnbounded<int>(); // Channelを読み取るときはReaderを使う var reader = channel.Reader; // メッセージの待受 WaitForChannelAsync(reader, this.GetCancellationTokenOnDestroy()).Forget(); // 書き込むときはWriteを使う var writer = channel.Writer; // IObserver<T>.OnNext() に相当 writer.TryWrite(1); writer.TryWrite(2); writer.TryWrite(3); // IObserver<T>.OnCompleted() に相当 writer.TryComplete(); // TryComplete()に例外を渡すと IObserver<T>.OnError() に相当 // writer.TryComplete(new Exception("")); } private async UniTaskVoid WaitForChannelAsync(ChannelReader<int> reader, CancellationToken token) { try { // 1回だけ読み取るならReadAsync var result1 = await reader.ReadAsync(token); // UniTask<int> Debug.Log(result1); // 完了するまで読み続けるなら ReadAllAsync // IObservable<T>.Subscribe() に相当 await reader.ReadAllAsync() // IUniTaskAsyncEnumerable<int> .ForEachAsync(x => Debug.Log(x), token); Debug.Log("Done"); } catch (Exception e) { Debug.LogException(e); } }AsyncReactiveProperty
AsyncReactiveProperty
はUniRx
のReactiveProperty
のUniTask
版です。
ベースとしてIUniTaskAsyncEnumerable<T>
が利用されています。
(ReactiveProperty
はベースがIObservable<T>
)基本的な使い方は
ReactiveProperty
と変わりません。AsyncReactivePropertyprivate void Start() { var token = this.GetCancellationTokenOnDestroy(); // AsyncReactiveProperty生成 var asyncReactiveProperty = new AsyncReactiveProperty<string>(null); // 待受開始 WaitForAsync(asyncReactiveProperty, token).Forget(); // Valueプロパティに値をセットすると // MoveNextAsync() が次に進む asyncReactiveProperty.Value = "Hello!"; // Dispose()するとこのAsyncReactivePropertyが完了する asyncReactiveProperty.Dispose(); } private async UniTaskVoid WaitForAsync( IReadOnlyAsyncReactiveProperty<string> asyncReadOnlyReactiveProperty, CancellationToken token) { // Valueプロパティで現在値を取得可能 var current = asyncReadOnlyReactiveProperty.Value; Debug.Log(current); // IUniTaskAsyncEnumerable<T>として扱える var result = await asyncReadOnlyReactiveProperty // null以外の値がセットされるのを待つ .FirstOrDefaultAsync(x => x != null, cancellationToken: token); Debug.Log(result); }UniTask.Linq
IUniTaskAsyncEnumerable<T>
にはLINQ
メソッドが用意されています。
使えるメソッドは次です。同期型
- AggregateAsync
- AllAsync
- AnyAsync
- AsUniTaskAsyncEnumerable
- AverageAsync
- Buffer
- Append
- Prepend
- Cast
- Concat
- ContainsAsync
- CountAsync
- DefaultIfEmpty
- Distinct
- DistinctUntilChanged
- Do
- ElementAtAsync
- ElementAtOrDefaultAsync
- Except
- FirstAsync
- FirstOrDefaultAsync
- ForEachAsync
- GroupBy
- GroupJoin
- Intersect
- Join
- LastAsync
- LastOrDefaultAsync
- LongCountAsync
- MaxAsync
- MinAsync
- OfType
- OrderBy
- OrderByDescending
- Reverse
- Select
- SelectMany
- SequenceEqualAsync
- SingleAsync
- SingleOrDefaultAsync
- Skip
- SkipLast
- SkipWhile
- SumAsync
- Take
- TakeLast
- TakeWhile
- ToArrayAsync
- ToDictionaryAsync
- ToHashSetAsync
- ToListAsync
- ToLookupAsync
- ToObservable
- Union
- Where
- Zip
- Queue
- SkipUntilCanceled
- TakeUntilCanceled
非同期型
AwaitAsync
がついているものはasync/await
を内部で利用できます。
- AggregateAwaitAsync
- AllAwaitAsync
- AnyAwaitAsync
- AverageAwaitAsync
- CountAwaitAsync
- DistinctAwait
- DistinctUntilChangedAwait
- DoAwait
- FirstAwaitAsync
- FirstOrDefaultAwaitAsync
- ForEachAwaitAsync
- GroupByAwait
- GroupJoinAwait
- JoinAwait
- LastAwaitAsync
- LastOrDefaultAwaitAsync
- LongCountAwaitAsync
- MaxAwaitAsync
- MinAwaitAsync
- OrderByAwait
- OrderByDescendingAwait
- SelectAwait
- SelectManyAwait
- SingleAwaitAsync
- SingleOrDefaultAwaitAsync
- SkipWhileAwait
- SumAwaitAsync
- TakeWhileAwait
- ToDictionaryAwaitAsync
- ToLookupAwaitAsync
- WhereAwait
- ZipAwait
こちらは
CancellationToken
を内部で必要とするときに使います。
- AggregateAwaitWithCancellationAsync
- AnyAwaitWithCancellationAsync
- AverageAwaitWithCancellationAsync
- CountAwaitWithCancellationAsync
- DistinctAwaitWithCancellation
- DistinctUntilChangedAwaitWithCancellation
- DoAwaitWithCancellation
- FirstAwaitWithCancellationAsync
- FirstOrDefaultAwaitWithCancellationAsync
- ForEachAwaitWithCancellationAsync
- GroupByAwaitWithCancellation
- GroupJoinAwaitWithCancellation
- JoinAwaitWithCancellation
- LastAwaitWithCancellationAsync
- LastOrDefaultAwaitWithCancellationAsync
- LongCountAwaitWithCancellationAsync
- MaxAwaitWithCancellationAsync
- MinAwaitWithCancellationAsync
- OrderByAwaitWithCancellation
- OrderByDescendingAwaitWithCancellation
- SelectAwaitWithCancellation
- SelectManyAwaitWithCancellation
- SingleAwaitWithCancellationAsync
- SingleOrDefaultAwaitWithCancellationAsync
- SkipWhileAwaitWithCancellation
- SumAwaitWithCancellationAsync
- TakeWhileAwaitWithCancellation
- ToDictionaryAwaitWithCancellationAsync
- ToLookupAwaitWithCancellationAsync
- WhereAwaitWithCancellation
- ZipAwaitWithCancellation
補足: Pushベースの
UniTaskAsyncEnumerable
の注意点
IUniTaskAsyncEnumerable<T>
はPull
型として動作します。
そのためメッセージの受信準備が整い、内部でイテレータのMoveNextAsync()
が実行されたタイミングで次のメッセージを取りに行きます。ですが、
UniTaskAsyncEnumerable
ではメッセージ発行がPush
なIUniTaskAsyncEnumerable<T>
も存在します(ややこしい)
UniTask.EveryUpdate()
AsyncReactiveProperty
AsyncTrigger
より生成したものuGUI
のイベントなどから変換したものこれらは
Push
されてきたイベントをIUniTaskAsyncEnumerable<T>
として提供します。
そのためMoveNextAsync()
とタイミングが合わなかった場合は、その間に発行されたイベントは取りこぼされるという点に注意が必要です。逆に、このイベントの取りこぼしを利用して処理を組むこともできます。
イベントの取りこぼしを利用した例// uGUIのボタンの連打防止 // 1回ボタンを押したら2秒間無反応になる // ForEachAwaitAsyncが待機中に発行されたメッセージは無視するという // 性質を利用している _button.OnClickAsAsyncEnumerable() .ForEachAwaitWithCancellationAsync(async (_, ct) => { Debug.Log("Clicked!"); await UniTask.Delay(2000, cancellationToken: ct); }, token);取りこぼしが嫌なら Queue() を使おう
Queue
はUniTask.Linq
が提供するLINQ
メソッドの1つです。
IUniTaskAsyncEnumerable<T>
に対して先にMoveNextAsync()
を実行し、その結果をキューに詰めて再度IUniTaskAsyncEnumerable<T>
として提供します。
(Observable
でいうところのPublish()
に相当します)
Queue
を使えばイベントの取りこぼしを防ぐことができるため、必要に応じて利用しましょう。Queueprivate void Start() { var token = this.GetCancellationTokenOnDestroy(); // AsyncReactiveProperty生成 var asyncReactiveProperty = new AsyncReactiveProperty<string>("Initialize!"); // 待受開始 WaitForAsync(asyncReactiveProperty, token).Forget(); // 値を設定 asyncReactiveProperty.Value = "Hello!"; asyncReactiveProperty.Value = "World!"; asyncReactiveProperty.Value = "Thank you!"; asyncReactiveProperty.Dispose(); } private async UniTaskVoid WaitForAsync( IReadOnlyAsyncReactiveProperty<string> asyncReadOnlyReactiveProperty, CancellationToken token) { // AsyncReactiveProperty はキューイングしてくれないので // タイミングによってはメッセージの取りこぼしが置きうる // 取りこぼしが嫌ならQueueを併用する await asyncReadOnlyReactiveProperty .Queue() // Queueを挟む .ForEachAwaitWithCancellationAsync(async (x, ct) => { Debug.Log(x); // 1秒待って次の値を取りに行く await UniTask.Delay(1000, cancellationToken: ct); }, token); }まとめ
C# 8.0
のIAsyncEnumerable
相当の処理をいち早くUnity
でも利用できるため、かなり期待が高いアップデートとなりました!
UniTaskAsyncEnumerable
とAsyncReactiveProperty
が刺さる人にはかなり刺さる機能だと思います。(自分はさっそく使いたい)ただ、これの登場によって
IObservable<T>
との使い分けが一層ややこしくなったとも思います。
UniRx
で苦戦していた人はおそらくUniTaskAsyncEnumerable
でも苦戦するような予感が…。
- 投稿日:2020-05-19T22:18:14+09:00
【C#】リバーシやテトリスとかに使える簡単なコンソール描画
目的
コンソール画面でアルゴリズムを視覚化するための描画処理の作成
目次
- 目標
- 作成
- 完成
- 応用
- まとめ
目標
- コンソール画面に四角形を複数表示ができる
- 縦と横の四角形の数を指定できる
- 並べた四角形の色を自由に設定できる
作成
using System; using System.Collections.Generic; using System.Linq; using System.Text; using System.Threading.Tasks; using System.Drawing; /// <summary> /// コンソール描画 /// </summary> namespace ConsoleDrawer { /// <summary> /// 四角形描画処理 /// </summary> public class SquareDrawer { private ConsoleColor[,] canvas; private int width; private int height; /// <summary> /// コンストラクタ /// </summary> /// <param name="width">幅</param> /// <param name="height">高さ</param> public SquareDrawer(int width, int height) { Console.CursorVisible = false; Console.BackgroundColor = ConsoleColor.Black; this.width = width; this.height = height; canvas = new ConsoleColor[height, width]; for (int i = 0; i < height; i++) { for (int j = 0; j < width; j++) { canvas[i, j] = ConsoleColor.Black; } } } /// <summary> /// 四角形描画 /// </summary> /// <param name="color">文字色</param> private void DrawSquare(ConsoleColor color) { Console.ForegroundColor = color; Console.Write("■"); } /// <summary> /// すべての四角形の描画 /// </summary> public void DrawAllSquare() { Console.ForegroundColor = ConsoleColor.Black; Console.SetCursorPosition(0, 0); for (int i = 0; i < height; i++) { for (int j = 0; j < width; j++) { DrawSquare(canvas[i, j]); } Console.Write("\n"); } } /// <summary> /// 文字色の指定 /// </summary> /// <param name="color">文字色</param> /// <param name="position">位置</param> public void SetColor(ConsoleColor color, Point position) { canvas[position.Y, position.X] = color; } /// <summary> /// 文字色の指定 /// </summary> /// <param name="color"></param> /// <param name="x">横の位置</param> /// <param name="y">縦の位置</param> public void SetColor(ConsoleColor color, int x, int y) { SetColor(color, new Point(x, y)); } } }完成
using System; using System.Threading; using System.Drawing; using ConsoleDrawer; namespace Test { class Program { static void Main(string[] args) { Console.Title = "テスト"; SquareDrawer drawer = new SquareDrawer(6, 6); while (true) { // アルゴリズムはこの中で動かす // 色を指定 drawer.SetColor(ConsoleColor.White, 0, 1); drawer.SetColor(ConsoleColor.Yellow, 0, 0); drawer.SetColor(ConsoleColor.Blue, 5, 0); drawer.SetColor(ConsoleColor.Green, 5, 5); drawer.DrawAllSquare(); Thread.Sleep(500); } } } }応用
- 四角形ではなく丸に
- リバーシにも使える
- 任意の図形に変更
- より広くいろいろなアルゴリズムを扱えるようになる?
まとめ
アルゴリズムを純粋に考えたい時ように今回のものをつくりました。
c#独自のものはあまり使っていないので、他の言語にも参考になればいいと思っています。
- 投稿日:2020-05-19T18:31:34+09:00
スキャンアプリケーション ITScan
1. はじめに
TwainDotNetによるスキャン - Qiitaを元に、TwainDotNetが内部で32ビットBitmapを作成する部分を24ビット以下にも対応 - Qiitaを組み合わせて、スキャンアプリケーションを作成しました。Windows用です。
BMPとPNGで保存可能です。
BTScanを参考にして、その機能の一部を作成しています。PNGの保存は.NET標準機能で行っていますが、ファイルサイズはBTScanの圧縮率0(低い)よりも小さくなりました。(自分が試したグレースケール画像で約1/2~1/3)このアプリケーションにはスキャン画像の表示機能はありませんが、画像ビューア ImageTest1 - Qiitaを監視モードで起動しておけば、リアルタイムに表示確認が可能です。
2. バイナリ
フリーウェアとします。無保証です。
3. ソース
ITScan_src_1.03_20200522.zip 最新版
GitHubリポジトリTwainDotNet_1.0.0_patch_8bit_support_20200522-2.zip
C#で記述しています。Visual Studio 2019でビルド可能です。
TwainDotNetに24ビット以下への対応の改造を行っています。4. 動作環境
.NET Framework 4.7.2以上+.NET Framework 3.5以上。
.NET Framework 4.7.2以上はWindows 10 April 2018 Update(1803)以上であればインストールされています。
ソースからリビルドすればおそらく.NET Framework 2.0以上くらいまで落とすことができます。
TwainDotNetが.NET Framework 3.5向けのため、おそらく.NET Framework 3.5以上も必要です。
32ビットアプリケーションです。
Windows 10 64ビット版で動作確認をしています。5. 気になる点
1) スキャンが終わったらスキャナのダイアログが閉じてしまいます。TwainDotNet自体の動作のようです。BTScanのようにスキャナのダイアログを開いたままにする方法はないものか…。
2) JPEGに対応していないのは、JPEGの品質指定のUIを作るのを面倒がっているからです。
3) ImageTest1の監視モードと併用すると、ImageTest1側で頻繁に読み込み失敗が表示されます。ImageTest1側での対処を検討中です。→ImageTest1 1.13 20200522で対応しました。6. 更新履歴
2020/05/22 1.03
1) TwainDotNetから返却されたBitmapの解放処理を追加。これにより、数ページ~十数ページ程度(スキャナや取り込みサイズによる)で連続スキャンが止まり、途中までしかファイルが保存されないのを修正。2020/05/22 1.02
1) TwainDotNetのビット数判定を厳密に変更。(24ビット以下→32ビット以外(64ビットを含む)、8ビット以下→24ビット未満)
2) アクセス・キーを追加。
3) カウンタの初期値を0から1に変更。
4) TwainDotNetにADFを使用するフラグのセットを削除。1.00と同じ動作に戻した。2020/05/20 1.01
1) 24ビットカラーの場合に32ビットで保存していたのを24ビットに変更。
2) TwainDotNetにADFを使用するフラグをセット(特に動作が変わっているように見えない)。→フラットベッドとADFの両方があるスキャナの場合に、毎回ADFになりそうなので、次回アップデートで削除予定。2020/05/19 1.00
1) 初版。
- 投稿日:2020-05-19T17:52:55+09:00
正しいプログラミングの学習方法について語る
今回は プログラミング学習について 見ていきます。
特に独学での学習者はこれらの点を気をつけて学習したほうがいいです。
1 完璧主義を目指してはいけない
プログラミングの学習において、完璧主義を目指すのはほんとに自殺行為といえます。
これは学習でもそうですけど、プロダクトを作るときも同じです。
最初は6割や7割ベースで 全然いいと思う。
特に独学でやっている人ほど、完璧主義を目指すのはやめましょう。
2 暗記も必要もない
学校の勉強のような、暗記も必要ないです。
覚えるのは基礎だけでいいわけで あとは
必要なときに 必要な分だけ覚えていくのがいいと思う。
なので メソッドやライブラリなどを全部覚える必要はないです。
3 小さい目標をつけるべし
わたしもプログラミングは全部独学で覚えたんですけど
独学の場合だと、やはりモチベーションが続かないんですよね。
モチベも続かないし、普通に挫折もすると思います。挫折しない人なんているんでしょうかね???
なので 小さいな目標を立てることが重要です。
例えば 1日 10ページすすめるとか
1日 1回はアウトプットするとか
サンプルコードをやるとかね。
因みに習慣の法則というものがありまして
21日間 続けることが出来れば、 そのあとも継続できるようになるので
とりあえず 3週間は続ける事が大事。
4 インプットよりも アウトプットを増やしなさい
インプット学習ってみんな普通にするんですよ。
でも アウトプットしている人はほんとに少ないです。
みんな、 Udemyとかの動画は見ているけど
それで終わっている人が多いからね。なので、アウトプットする習慣を身に付けることが最も大事。
インプットよりも、どうやってアウトプットできるかを
考えるべし。初心者であれば、 パイザのコードクロニクルなどが
おすすめです。5 とりあえず ググり癖はつけるべし
分からなければ すぐに調べる。
調べ方は色々あるけど
目的で検索するか、 コードをコピペしていくのが基本。
で これは 誰かに質問するときも同じで
質問するときも、 ちゃんと調べてから質問すること。あと 何でもかんでも、質問すればいいってわけじゃないからね。
あんまり、質問ばっかりしていると嫌われるよん。
PS
C#プログラミングマスター講座も
発売中でーす。只今70%OFFセールやってます。
C#が分からないと Unityもできるようにならないので
Unityをやるひとにも おすすめです。
- 投稿日:2020-05-19T17:31:50+09:00
【VisualStudio】コードジェネレータ―が働かなかった【備忘録】
はじめに
VisualStudio2019でWeb APIを作成しようとしたとき、コントローラーを作ろうと思ったら以下のエラーが出て作成ができませんでした。
これの解決方法を備忘録として書いておきます。
環境
Windows10
VisualStudio20191. エラーの原因
とりあえず出たエラーに書いてある文言「パッケージの復元に失敗しました。のパッケージの変更をロールバックします。」をコピペして調べましたが、どれをやっても解決せず、、、
原因は「コードジェネレータを実行中にエラーが発生しました。」の方にありました。
VisualStudioがコントローラをスキャフォールドで作成してくれる時には、
Microsoft.VisualStudio.Web.CodeGeneration.Design
というパッケージを使います。(使うものはほかにもあります。)コントローラを作成しようとすると、パッケージマネージャであるNuGetが勝手にこれらのパッケージをダウンロードしてきてくれて、それを使ってくれます。
そのダウンロードがなぜかできていなかったのが原因でした。
2. 解決策
上部のメニューからツール→NuGetパッケージマネージャ→パッケージマネージャの設定を選択して、設定を開く
パッケージソースを選択
ここにパッケージのダウンロード元のURLが記述されているはずなのに記述されていなかったことが原因でした。右上のプラスボタンから新規追加し、
- 名前:nuget.org
- ソース:https://api.nuget.org/v3/index.json
と記述することで完了です。
これによってダウンロード元が指定され、ここからパッケージを持ってきてくれます。
さいごに
つい最近VisualStudioをインストールしたのですが、普通最初からURL指定されていませんか?
VisualStudioを使っている人が全員この作業をしたとは思えない。
原因に気づくまでにかなり時間がかかってしまいましたが、解決してよかったです。Twitter↓
@ruemura3
- 投稿日:2020-05-19T14:14:29+09:00
眺めて覚える C# Xamarin Forms(14) 連絡先 参照
今回は、連絡先のアクセスがテーマです。
連絡先をアプリに読み込む締めには、許可が必要になります。
プロジェクト作成の手順
Propertiesに変更を加えます。
必要があれば書き込み許可を与えます。
マニフェストには、下記のように自動的に追加されます。
AndroidManifest.xml<?xml version="1.0" encoding="utf-8"?> <manifest xmlns:android="http://schemas.android.com/apk/res/android" android:versionCode="1" android:versionName="1.0" package="com.companyname.projectcontact" android:installLocation="auto"> <uses-sdk android:minSdkVersion="21" android:targetSdkVersion="28" /> <application android:label="ProjectContact.Android"></application> <uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" /> <uses-permission android:name="android.permission.READ_CONTACTS" /> <uses-permission android:name="android.permission.WRITE_CONTACTS" /> </manifest>MainActivity.csに許可のためのルーチンを追加します。①②
MainActivity.csusing System; using Android.App; using Android.Content.PM; using Android.Runtime; using Android.Views; using Android.Widget; using Android.OS; //許可ダイアログ用① using Android.Support.V4.Content; using Android.Support.V4.App; using Android; namespace ProjectContact.Droid { [Activity(Label = "ProjectContact", Icon = "@mipmap/icon", Theme = "@style/MainTheme", MainLauncher = true, ConfigurationChanges = ConfigChanges.ScreenSize | ConfigChanges.Orientation)] public class MainActivity : global::Xamarin.Forms.Platform.Android.FormsAppCompatActivity { protected override void OnCreate(Bundle savedInstanceState) { TabLayoutResource = Resource.Layout.Tabbar; ToolbarResource = Resource.Layout.Toolbar; base.OnCreate(savedInstanceState); // 許可ダイアログのために追加② if (ContextCompat.CheckSelfPermission(this, Manifest.Permission.ReadContacts) != (int)Permission.Granted) ActivityCompat.RequestPermissions(this, new string[] { Manifest.Permission.ReadContacts }, 0); Xamarin.Essentials.Platform.Init(this, savedInstanceState); global::Xamarin.Forms.Forms.Init(this, savedInstanceState); LoadApplication(new App()); } public override void OnRequestPermissionsResult(int requestCode, string[] permissions, [GeneratedEnum] Android.Content.PM.Permission[] grantResults) { Xamarin.Essentials.Platform.OnRequestPermissionsResult(requestCode, permissions, grantResults); base.OnRequestPermissionsResult(requestCode, permissions, grantResults); } } }依存関係にNugetで追加します。
Xamarin.Forms.Contactsを追加します。
以下の例は、xamlを用いずプログラム的にデザインを作成しています。
MainPage.xaml.csusing System; using System.Collections.Generic; using System.ComponentModel; using System.Linq; using System.Text; using System.Threading.Tasks; using Xamarin.Forms; namespace ProjectContact { // Learn more about making custom code visible in the Xamarin.Forms previewer // by visiting https://aka.ms/xamarinforms-previewer [DesignTimeVisible(false)] public partial class MainPage : ContentPage { public MainPage() { InitializeComponent(); #pragma warning disable CS4014 // この呼び出しは待機されなかったため、現在のメソッドの実行は呼び出しの完了を待たずに続行されます GetContacs(); } class MyContact { public string Name { get; set; } = ""; public string Email { get; set; } = ""; public string Number { get; set; } = ""; } async Task GetContacs() { var contacts = await Plugin.ContactService.CrossContactService.Current.GetContactListAsync(); var list = new List<MyContact>(); foreach (var x in contacts) { list.Add(new MyContact { Name = x.Name ?? "", Email = x.Email ?? "", Number = (x.Numbers.Count > 0) ? x.Numbers[0] : "" }); } var tmp = new DataTemplate(() => { var grid = new Grid() { Margin = 0, BackgroundColor = Color.Black }; grid.RowDefinitions.Add(new RowDefinition { Height = new GridLength(1, GridUnitType.Star) }); grid.RowDefinitions.Add(new RowDefinition { Height = new GridLength(1, GridUnitType.Star) }); grid.ColumnDefinitions.Add(new ColumnDefinition { Width = new GridLength(1, GridUnitType.Star) }); grid.ColumnDefinitions.Add(new ColumnDefinition { Width = new GridLength(1, GridUnitType.Star) }); var name = new Label { TextColor = Color.White, FontSize = 18 }; name.SetBinding(Label.TextProperty, "Name"); var email = new Label() { TextColor = Color.White }; email.SetBinding(Label.TextProperty, "Email"); var number = new Label() { TextColor = Color.White }; number.SetBinding(Label.TextProperty, "Number"); grid.Children.Add(name, 0, 0); grid.Children.Add(number, 0, 1); grid.Children.Add(email, 1, 1); return new ViewCell { View = grid }; }); var lv = new ListView() { ItemsSource = list, ItemTemplate = tmp }; Content = new Xamarin.Forms.ScrollView() { Margin = 1, Orientation = ScrollOrientation.Vertical, Content = lv }; } } }実行結果
- 投稿日:2020-05-19T05:12:29+09:00
macOS上でSQL Serverを使用してC#アプリを作成する
はじめに
この記事では、Microsoft 社が公開している Build an app using SQL Server の内容に従い、SQL Server を使用した C# アプリを作成します。
環境
- OS: macOS Catalina バージョン 10.15.4
- SQL Server: SQL Server 2019
- .NET Core: 3.1 LTS
環境のセットアップ
ここでは、SQL Server を Docker 上で取得します。その後、SQL Server で .NET Core アプリを作成するために必要な依存関係をインストールします。
SQL Server のインストール
- macOS で SQL Server を実行するには、SQL Server on Linux の Docker イメージ を使用します。そのためには、Docker for Mac をインストールする必要があります。
- Docker 環境に最低 4GB のメモリを設定し、パフォーマンスを評価したい場合は複数のコアを追加することも検討します。これは、メニューバーの [環境設定] -> [詳細設定] オプションで行うことができます。
- 新しいターミナルプロンプトを起動し、以下のコマンドを使用して SQL Server on Linux Docker イメージをダウンロードして起動します。
SA_PASSWORD の部分は特殊文字を使用した強力なパスワードを使用するように書き換えてください。
sudo docker pull microsoft/mssql-server-linux:2017-latest docker run -e 'HOMEBREW_NO_ENV_FILTERING=1' -e 'ACCEPT_EULA=Y' -e 'SA_PASSWORD=yourStrong(!)Password' -p 1433:1433 -d microsoft/mssql-server-linux筆者は、Docker Desktop をインストールすると一緒に利用可能になる、Docker Compose を利用して、
SQL Server 2019 on Linux
をインストールしています。docker-compose.yaml については、以下の GitHub リポジトリを参照してください。なお、
ウェブ上で公開されている Docker イメージ
の URL は、Ubuntu ベースの SQL Server 2017 on Linux の Docker イメージ です。SQL Server 2019
では、従来の Ubuntu ベースに加え、RHEL ベース の SQL Server on Linux の Docker イメージ もサポートされるようになりました。
Ubuntu ベースを利用するか、RHEL ベースを利用するかは、好きな方を選択してください。(SQL Server における機能に差はありません)Homebrew と .NET Core のインストール
すでに .NET Core 3.1 LTS がインストールされている場合は、このステップをスキップしてください。
公式インストーラー をダウンロードして、.NET Coreをインストールします。インストーラーは、Build apps - SDK のものを選択してください。.NET Core 3.1.x SDK および、.NET Core ランタイムを一緒にインストールできます。
- ASP.NET Core Runtime
- Desktop Runtime
- .NET Core Runtime (上の2つを一緒にしたもの)
.NET Core では、作成したアプリを実行するためのランタイムが、種類によって分けられています。
ダウンロードサイト上では分かれて表示されているため、悩んでしまう可能性がありますが、ここでは気にしないでください。なお、
本家のサイトにあるリンクは .NET Core 2.0 のダウンロードリンク
になっています。これは既にサポート切れ、かつ LTS ではないため、最新の .NET Core 3.1.x をダウンロードし、インストールするようにしてください。.NET Core 3.1 は LTS バージョンになります。なお、筆者は、SQL Server 2019 と同様、Docker Compose を使用して、.NET Core 3.1 のコンテナーを作成し、開発を進めています。docker-compose.yaml については、以下の GitHub リポジトリを参照してください。
SQL Server および .NET Core 3.1 を Docker 上で利用する場合は、同じ docker-compose.yaml ファイル内に記述します。
container_name
を記述することで、コンテナ名を使ってコンテナ同士の相互通信が可能になります。docker-compose.yaml(例)version: '3' services: app: image: mcr.microsoft.com/dotnet/core/sdk:latest container_name: dotnetcoreapp tty: true ports: - 10080:80 volumes: - ./src:/src working_dir: "/src" mssql: image: mcr.microsoft.com/mssql/rhel/server:2019-latest container_name: 'mssql2019' environment: - MSSQL_SA_PASSWORD=databaseadmin@1 - ACCEPT_EULA=Y ports: - 1433:1433 # volumes: # Mounting a volume does not work on Docker for Mac # - ./mssql/log:/var/opt/mssql/log # - ./mssql/data:/var/opt/mssql/dataSQL Server を使った C# アプリケーションを作成
ここでは、以下、2 つのシンプルな C# アプリを作成します。
- 基本的な Insert、Update、Delete、Select を実行するアプリ
- .NET Core の ORM フレームワークの中でも特に人気のある Entity Framework Core を利用してInsert、Update、Delete、Select を実行するアプリ
SQL Server に接続してクエリを実行する C# アプリを作成
開発を行うワークディレクトリに移動し、新しい .NET Core プロジェクトを作成します。
基本的な .NET Core の Program.cs と csproj ファイルを含むプロジェクトディレクトリが作成されます。cd ~/ dotnet new console -o SqlServerSampleDocker Compose で行う場合は、以下の通りです。
docker-compose run --rm app dotnet new console -o SqlServerSampleSqlServerSample.csproj というファイルが SqlServerSample ディレクトリ以下に作成されます。
任意のテキストエディタで SqlServerSample.csproj ファイルを開き、コードを以下の通りに書き換え、System.Data.SqlClient をプロジェクトに追加します。保存してファイルを閉じます。SqlServerSample.csproj<Project Sdk="Microsoft.NET.Sdk"> <PropertyGroup> <OutputType>Exe</OutputType> <TargetFramework>netcoreapp3.1</TargetFramework> </PropertyGroup> <ItemGroup> <PackageReference Include="System.Data.SqlClient" Version="4.8.1" /> </ItemGroup> </Project>SqlServerSample ディレクトリ以下にある Program.cs ファイルを開き、コードを以下の通りに書き換え、保存してファイルを閉じます。
ユーザー名とパスワードを自分のものに置き換えることを忘れないでください。
Program.csusing System; using System.Data.SqlClient; namespace SqlServerSample { class Program { static void Main(string[] args) { try { // 接続文字列の構築 SqlConnectionStringBuilder builder = new SqlConnectionStringBuilder(); builder.DataSource = "localhost"; // 接続先の SQL Server インスタンス builder.UserID = "sa"; // 接続ユーザー名 builder.Password = "your_password"; // 接続パスワード builder.InitialCatalog = "master"; // 接続するデータベース(ここは変えないでください) // builder.ConnectTimeout = 60000; // 接続タイムアウトの秒数(ms) デフォルトは 15 秒 // SQL Server に接続 Console.Write("SQL Server に接続しています... "); using (SqlConnection connection = new SqlConnection(builder.ConnectionString)) { connection.Open(); Console.WriteLine("接続成功。"); } } catch (SqlException e) { Console.WriteLine(e.ToString()); } Console.WriteLine("すべてが完了しました。任意のキーを押してアプリを終了します..."); Console.ReadKey(true); } } }SqlServerSample ディレクトリに戻り、以下のコマンドを実行して csproj 内の依存関係を復元します。
cd ~/SqlServerSample dotnet restore
完了したら、ビルド実行を行います。
dotnet runなお、Docker Compose で実行している場合は、以下のようなコマンドを実行することで上記を実現できます。
docker-compose run -w /src/SqlServerSample --rm app dotnet restore docker-compose run -w /src/SqlServerSample --rm app dotnet runこれで、SQL Server に接続を行うコンソールアプリができました。ただし、このアプリでは単にデータベースへの接続だけを行っているだけで、クエリは実行していません。
次に、Program.cs 内にコードを追加して、データベースやテーブルの作成、INSERT/UPDATE/DELETE/SELECT などのクエリを実行するように変更します。
ユーザー名とパスワードは自分のものに置き換えることを忘れないでください。
書き換えた後、ファイルを保存し、プロジェクトをビルドして実行します。Program.csusing System; using System.Text; using System.Data.SqlClient; namespace SqlServerSample { class Program { static void Main(string[] args) { try { Console.WriteLine("SQL Server に接続し、Create、Read、Update、Delete 操作のデモを行います。"); // 接続文字列の構築 SqlConnectionStringBuilder builder = new SqlConnectionStringBuilder(); builder.DataSource = "localhost"; // 接続先の SQL Server インスタンス builder.UserID = "sa"; // 接続ユーザー名 builder.Password = "your_password"; // 接続パスワード builder.InitialCatalog = "master"; // 接続するデータベース(ここは変えないでください) // builder.ConnectTimeout = 60000; // 接続タイムアウトの秒数(ms) デフォルトは 15 秒 // SQL Server に接続 Console.Write("SQL Server に接続しています... "); using (SqlConnection connection = new SqlConnection(builder.ConnectionString)) { connection.Open(); Console.WriteLine("接続成功。"); // サンプルデータベースの作成 Console.Write("既に作成されている SampleDB データベースを削除し、再作成します... "); String sql = "DROP DATABASE IF EXISTS [SampleDB]; CREATE DATABASE [SampleDB]"; using (SqlCommand command = new SqlCommand(sql, connection)) { // command.CommandTimeout = 60000; // コマンドがタイムアウトする場合は秒数を変更(ms) デフォルトは 30秒 command.ExecuteNonQuery(); Console.WriteLine("SampleDB データベースを作成しました。"); } // テーブルを作成しサンプルデータを登録 Console.Write("サンプルテーブルを作成しデータを登録します。任意のキーを押して続行します..."); Console.ReadKey(true); StringBuilder sb = new StringBuilder(); sb.Append("USE SampleDB; "); sb.Append("CREATE TABLE Employees ( "); sb.Append(" Id INT IDENTITY(1,1) NOT NULL PRIMARY KEY, "); sb.Append(" Name NVARCHAR(50), "); sb.Append(" Location NVARCHAR(50) "); sb.Append("); "); sb.Append("INSERT INTO Employees (Name, Location) VALUES "); sb.Append("(N'Jared', N'Australia'), "); sb.Append("(N'Nikita', N'India'), "); sb.Append("(N'Tom', N'Germany'); "); sql = sb.ToString(); using (SqlCommand command = new SqlCommand(sql, connection)) { // command.CommandTimeout = 60000; // コマンドがタイムアウトする場合は秒数を変更(ms) デフォルトは 30秒 command.ExecuteNonQuery(); Console.WriteLine("作成完了"); } // INSERT デモ Console.Write("テーブルに新しい行を挿入するには、任意のキーを押して続行します..."); Console.ReadKey(true); sb.Clear(); sb.Append("INSERT Employees (Name, Location) "); sb.Append("VALUES (@name, @location);"); sql = sb.ToString(); using (SqlCommand command = new SqlCommand(sql, connection)) { // command.CommandTimeout = 60000; // コマンドがタイムアウトする場合は秒数を変更(ms) デフォルトは 30秒 command.Parameters.AddWithValue("@name", "Jake"); command.Parameters.AddWithValue("@location", "United States"); int rowsAffected = command.ExecuteNonQuery(); Console.WriteLine(rowsAffected + " 行 挿入されました"); } // UPDATE デモ String userToUpdate = "Nikita"; Console.Write("ユーザー '" + userToUpdate + "' の 'Location' を更新するには、任意のキーを押して続行します..."); Console.ReadKey(true); sb.Clear(); sb.Append("UPDATE Employees SET Location = N'United States' WHERE Name = @name"); sql = sb.ToString(); using (SqlCommand command = new SqlCommand(sql, connection)) { // command.CommandTimeout = 60000; // コマンドがタイムアウトする場合は秒数を変更(ms) デフォルトは 30秒 command.Parameters.AddWithValue("@name", userToUpdate); int rowsAffected = command.ExecuteNonQuery(); Console.WriteLine(rowsAffected + " 行 更新されました"); } // DELETE デモ String userToDelete = "Jared"; Console.Write("ユーザー '" + userToDelete + "' を削除するには、任意のキーを押して続行します..."); Console.ReadKey(true); sb.Clear(); sb.Append("DELETE FROM Employees WHERE Name = @name;"); sql = sb.ToString(); using (SqlCommand command = new SqlCommand(sql, connection)) { // command.CommandTimeout = 60000; // コマンドがタイムアウトする場合は秒数を変更(ms) デフォルトは 30秒 command.Parameters.AddWithValue("@name", userToDelete); int rowsAffected = command.ExecuteNonQuery(); Console.WriteLine(rowsAffected + " 行 削除されました"); } // READ デモ Console.WriteLine("テーブルからデータを読み取るには、任意のキーを押して続行します..."); Console.ReadKey(true); sql = "SELECT Id, Name, Location FROM Employees;"; using (SqlCommand command = new SqlCommand(sql, connection)) { using (SqlDataReader reader = command.ExecuteReader()) { while (reader.Read()) { Console.WriteLine("{0} {1} {2}", reader.GetInt32(0), reader.GetString(1), reader.GetString(2)); } } } } } catch (SqlException e) { Console.WriteLine(e.ToString()); } Console.WriteLine("すべて完了しました。任意のキーを押して終了します..."); Console.ReadKey(true); } } }これで、macOS 上の .NET Core を使って、初めて C# + SQL Server アプリを作成できました。次は、ORM を使って C# アプリを作成します。
.NET Core で Entity Framework Core ORM を使用して SQL Server に接続する C# アプリを作成
ワークディレクトリに戻り、新しい.NET Coreプロジェクトを作成します。
cd ~/ dotnet new console -o SqlServerEFSampleDocker Compose で行う場合は、以下の通りです。
docker-compose run --rm app dotnet new console -o SqlServerSampleEFSqlServerEFSample.csproj というファイルが SqlServerEFSample ディレクトリ以下に作成されます。
任意のテキストエディタで SqlServerEFSample.csproj ファイルを開き、コードを以下の通りに書き換え、Entity Framework Core をプロジェクトに追加します。保存してファイルを閉じます。SqlServerEFSample.csproj<Project Sdk="Microsoft.NET.Sdk"> <PropertyGroup> <OutputType>Exe</OutputType> <TargetFramework>netcoreapp3.1</TargetFramework> </PropertyGroup> <ItemGroup> <PackageReference Include="System.Data.SqlClient" Version="4.8.1" /> <PackageReference Include="Microsoft.EntityFrameworkCore.SqlServer" Version="3.1.4" /> </ItemGroup> </Project>このサンプルでは、2つのテーブルを作成します。1つ目は「ユーザー」に関するデータを保持し、もう1つは「タスク」に関するデータを保持するものです。
User.cs を作成します。
User クラスを定義します。SqlServerEFSample ディレクトリ以下に User.cs ファイルを作成します。
このクラスは、User テーブルに紐づくモデルのクラスです。書き換えた後、ファイルを保存して閉じます。
この時点では、Task クラスがないためコンパイルエラーとなりますが、問題ありません。
User.csusing System; using System.Collections.Generic; namespace SqlServerEFSample { public class User { public int UserId { get; set; } public String FirstName { get; set; } public String LastName { get; set; } public virtual IList<Task> Tasks { get; set; } public String GetFullName() { return this.FirstName + " " + this.LastName; } public override string ToString() { return "User [id=" + this.UserId + ", name=" + this.GetFullName() + "]"; } } }Task.cs を作成します。
Task クラスを定義します。SqlServerEFSample ディレクトリ以下に Task.cs ファイルを作成します。
このクラスは、Task テーブルに紐づくモデルのクラスです。書き換えた後、ファイルを保存して閉じます。Task.csusing System; namespace SqlServerEFSample { public class Task { public int TaskId { get; set; } public string Title { get; set; } public DateTime DueDate { get; set; } public bool IsComplete { get; set; } public virtual User AssignedTo { get; set; } public override string ToString() { return "Task [id=" + this.TaskId + ", title=" + this.Title + ", dueDate=" + this.DueDate.ToString() + ", IsComplete=" + this.IsComplete + "]"; } } }EFSampleContext.cs を作成します。
EFSampleContext クラスを定義します。SqlServerEFSample ディレクトリ以下に EFSampleContext.cs ファイルを作成します。
このクラスは、Entity Framework Core を使用し、.NET オブジェクトを利用してデータのクエリ、挿入、更新、および削除を行うためのクラスです。User クラスと Task クラスを使用しています。
書き換えた後、ファイルを保存して閉じます。EFSampleContext.csusing Microsoft.EntityFrameworkCore; namespace SqlServerEFSample { public class EFSampleContext : DbContext { string _connectionString; public EFSampleContext(string connectionString) { this._connectionString = connectionString; } protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder) { optionsBuilder.UseSqlServer(this._connectionString); } public DbSet<User> Users { get; set; } public DbSet<Task> Tasks { get; set; } } }Entity Framework (.NET Framework) と違う点としては、OnConfiguring メソッドが新たにオーバーライドされ、逆に Database.SetInitializer(IDatabaseInitializer) を EFSampleContext のコンストラクタ内で指定しなくなっている点です。
最後に Program.cs を更新します。これまで作成したクラスを使用するための設定を行います。
ユーザー名とパスワードを自分のものに更新することを忘れないでください。
保存してファイルを閉じます。Program.csusing System; using System.Linq; using System.Data.SqlClient; using System.Collections.Generic; namespace SqlServerEFSample { class Program { static void Main(string[] args) { Console.WriteLine("** Entity Framework Core と SQL Server を使用した C# CRUD のサンプル **\n"); try { // 接続文字列を構築 SqlConnectionStringBuilder builder = new SqlConnectionStringBuilder(); builder.DataSource = "localhost"; // 接続先の SQL Server インスタンス builder.UserID = "sa"; // 接続ユーザー名 builder.Password = "your_password"; // 接続パスワード builder.InitialCatalog = "EFSampleDB";// 接続するデータベース(ここは変えないでください) // builder.ConnectTimeout = 60000; // 接続タイムアウトの秒数(ms) デフォルトは 15 秒 using (EFSampleContext context = new EFSampleContext(builder.ConnectionString)) { context.Database.EnsureDeleted(); context.Database.EnsureCreated(); Console.WriteLine("C#のクラスからデータベーススキーマを作成しました。"); // Create デモ: ユーザーインスタンスを作成し、データベースに保存 User newUser = new User { FirstName = "Anna", LastName = "Shrestinian" }; context.Users.Add(newUser); context.SaveChanges(); Console.WriteLine("\n作成されたユーザー: " + newUser.ToString()); // Create デモ: タスクインスタンスを作成し、データベースに保存 Task newTask = new Task() { Title = "Ship Helsinki", IsComplete = false, DueDate = DateTime.Parse("04-01-2017") }; context.Tasks.Add(newTask); context.SaveChanges(); Console.WriteLine("\nCreated Task: " + newTask.ToString()); // Association demo: Assign task to user newTask.AssignedTo = newUser; context.SaveChanges(); Console.WriteLine("\n作成されたタスク: '" + newTask.Title + "' 割り当てられたユーザー: '" + newUser.GetFullName() + "'"); // Read デモ: ユーザー 'Anna' に割り当てられた未完了のタスクを見つける Console.WriteLine("\n'Anna' に割り当てられた未完了のタスク:"); var query = from t in context.Tasks where t.IsComplete == false && t.AssignedTo.FirstName.Equals("Anna") select t; foreach(var t in query) { Console.WriteLine(t.ToString()); } // Update デモ: タスクの '期限' を変更 Task taskToUpdate = context.Tasks.First(); // 最初のタスクを取得 Console.WriteLine("\nタスクをアップデート中: " + taskToUpdate.ToString()); taskToUpdate.DueDate = DateTime.Parse("06-30-2016"); context.SaveChanges(); Console.WriteLine("変更された期限: : " + taskToUpdate.ToString()); // Delete デモ: 2016年が期限になっているすべてのタスクを削除 Console.WriteLine("\n期限が2016年になっているすべてのタスクを削除します。"); DateTime dueDate2016 = DateTime.Parse("12-31-2016"); query = from t in context.Tasks where t.DueDate < dueDate2016 select t; foreach(Task t in query) { Console.WriteLine("Deleting task: " + t.ToString()); context.Tasks.Remove(t); } context.SaveChanges(); // 'Delete' 操作の後にタスクを表示 - 0個のタスクがあるはず Console.WriteLine("\n削除後のタスク:"); List<Task> tasksAfterDelete = (from t in context.Tasks select t).ToList<Task>(); if (tasksAfterDelete.Count == 0) { Console.WriteLine("[なし]"); } else { foreach (Task t in query) { Console.WriteLine(t.ToString()); } } } } catch (Exception e) { Console.WriteLine(e.ToString()); } Console.WriteLine("すべて完了しました。任意のキーを押して終了します..."); Console.ReadKey(true); } } }EFSampleContext クラスのコンストラクタで Database.SetInitializer(IDatabaseInitializer) を行っていないため、Program.cs 内で context.Database.EnsureDeleted() と context.Database.EnsureCreated() が行われていますね。
SqlServerSampleEF ディレクトリに戻り、以下のコマンドを実行して csproj 内の依存関係を復元します。
cd ~/SqlServerEFSample dotnet restore
完了したら、ビルド実行を行います。
dotnet runDocker Compose で実行する場合は、以下のコマンドを実行してください。
docker-compose run -w /src/SqlServerEFSample --rm app dotnet restore docker-compose run -w /src/SqlServerEFSample --rm app dotnet runこれで、2つ目の C# アプリの作成が終わりました。最後に、SQL Server の カラムストア機能を使って C# アプリを高速化する方法について学びます。
C# アプリを 100 倍速にする
これまでで基本的なことは理解できたと思います。最後は、SQL Server を使用してアプリをより良くする方法を見てみます。このモジュールでは、カラムストアインデックスの簡単な例と、カラムストアインデックスがどのようにデータ処理速度を向上させるかを確認します。カラムストアインデックスは、従来の列ストアインデックスに比べて、分析ワークロードでは最大 100 倍のパフォーマンス向上、データ圧縮では最大 10 倍のパフォーマンス向上を実現できます。
カラムストアインデックスの機能を確認するために、500 万行のサンプルデータベースとサンプルテーブルを作成し、カラムストアインデックスを追加する前と後の簡単なクエリを実行する C# アプリケーションを作成します。
ワークディレクトリに戻り、新しい.NET Coreプロジェクトを作成します。
cd ~/ dotnet new console -o SqlServerColumnstoreSampleDocker Compose で行う場合は、以下の通りです。
docker-compose run --rm app dotnet new console -o SqlServerColumnstoreSampleSqlServerColumnstoreSample.csproj というファイルが SqlServerColumnstoreSample ディレクトリ以下に作成されます。
任意のテキストエディタで SqlServerColumnstoreSample.csproj ファイルを開き、コードを以下の通りに書き換え、System.Data.SqlClient をプロジェクトに追加します。保存してファイルを閉じます。SqlServerColumnstoreSample.csproj<Project Sdk="Microsoft.NET.Sdk"> <PropertyGroup> <OutputType>Exe</OutputType> <TargetFramework>netcoreapp3.1</TargetFramework> </PropertyGroup> <ItemGroup> <PackageReference Include="System.Data.SqlClient" Version="4.8.1" /> </ItemGroup> </Project>Program.cs の内容を書き換えます。
ユーザー名とパスワードは自分のものに置き換えることを忘れないでください。
保存してファイルを閉じます。Program.csusing System; using System.Collections.Generic; using System.Data.SqlClient; using System.Linq; using System.Text; using System.Threading.Tasks; namespace SqlServerColumnstoreSample { class Program { static void Main(string[] args) { try { Console.WriteLine("*** SQL Server カラムストアのデモ ***"); // 接続文字列の構築 SqlConnectionStringBuilder builder = new SqlConnectionStringBuilder(); builder.DataSource = "localhost"; // 接続先の SQL Server インスタンス builder.UserID = "sa"; // 接続ユーザー名 builder.Password = "your_password"; // 接続パスワード builder.InitialCatalog = "master"; // 接続するデータベース(ここは変えないでください) // builder.ConnectTimeout = 60000; // 接続タイムアウトの秒数(ms) デフォルトは 15 秒 // SQL Server に接続 Console.Write("SQL Serverへ接続しています... "); using (SqlConnection connection = new SqlConnection(builder.ConnectionString)) { connection.Open(); Console.WriteLine("接続完了。"); // サンプルデータベースの作成 Console.Write("'SampleDB' を再作成しています... "); String sql = "DROP DATABASE IF EXISTS [SampleDB]; CREATE DATABASE [SampleDB]"; using (SqlCommand command = new SqlCommand(sql, connection)) { command.CommandTimeout = 60000; // コマンドがタイムアウトする場合は秒数を変更(ms) デフォルトは 30秒 command.ExecuteNonQuery(); Console.WriteLine("完了。"); } // 'Table_with_5M_rows' テーブルに500万行を挿入 Console.Write("テーブル 'Table_with_5M_rows' に500万行を挿入します。1分ほどかかりますが、お待ちください... "); StringBuilder sb = new StringBuilder(); sb.Append("USE SampleDB; "); sb.Append("WITH a AS (SELECT * FROM (VALUES(1),(2),(3),(4),(5),(6),(7),(8),(9),(10)) AS a(a))"); sb.Append("SELECT TOP(5000000)"); sb.Append("ROW_NUMBER() OVER (ORDER BY a.a) AS OrderItemId "); sb.Append(",a.a + b.a + c.a + d.a + e.a + f.a + g.a + h.a AS OrderId "); sb.Append(",a.a * 10 AS Price "); sb.Append(",CONCAT(a.a, N' ', b.a, N' ', c.a, N' ', d.a, N' ', e.a, N' ', f.a, N' ', g.a, N' ', h.a) AS ProductName "); sb.Append("INTO Table_with_5M_rows "); sb.Append("FROM a, a AS b, a AS c, a AS d, a AS e, a AS f, a AS g, a AS h;"); sql = sb.ToString(); using (SqlCommand command = new SqlCommand(sql, connection)) { command.CommandTimeout = 60000; // コマンドがタイムアウトする場合は秒数を変更(ms) デフォルトは 30秒 command.ExecuteNonQuery(); Console.WriteLine("完了。"); } // カラムストアインデックスなしで SQL クエリを実行 double elapsedTimeWithoutIndex = SumPrice(connection); Console.WriteLine("カラムストアインデックスなしのクエリ時間: " + elapsedTimeWithoutIndex + "ms"); // カラムストアインデックスを追加 Console.Write("'Table_with_5M_rows' テーブルにカラムストアインデックスを追加中... "); sql = "CREATE CLUSTERED COLUMNSTORE INDEX columnstoreindex ON Table_with_5M_rows;"; using (SqlCommand command = new SqlCommand(sql, connection)) { command.CommandTimeout = 60000; // コマンドがタイムアウトする場合は秒数を変更(ms) デフォルトは 30秒 command.ExecuteNonQuery(); Console.WriteLine("完了。"); } // カラムストアインデックスが追加された後、再度同じ SQL クエリを実行 double elapsedTimeWithIndex = SumPrice(connection); Console.WriteLine("カラムストアありのクエリ時間: " + elapsedTimeWithIndex + "ms"); // カラムストアインデックスの追加によるパフォーマンス向上を計算 Console.WriteLine("カラムストアインデックスによる性能向上: " + Math.Round(elapsedTimeWithoutIndex / elapsedTimeWithIndex) + "x!"); } Console.WriteLine("すべて完了しました。任意のキーを押して終了します..."); Console.ReadKey(true); } catch (Exception e) { Console.WriteLine(e.ToString()); } } public static double SumPrice(SqlConnection connection) { String sql = "SELECT SUM(Price) FROM Table_with_5M_rows"; long startTicks = DateTime.Now.Ticks; using (SqlCommand command = new SqlCommand(sql, connection)) { try { command.CommandTimeout = 60000; // コマンドがタイムアウトする場合は秒数を変更(ms) デフォルトは 30秒 var sum = command.ExecuteScalar(); TimeSpan elapsed = TimeSpan.FromTicks(DateTime.Now.Ticks) - TimeSpan.FromTicks(startTicks); return elapsed.TotalMilliseconds; } catch (Exception e) { Console.WriteLine(e.ToString()); } } return 0; } } }SqlServerColumnstoreSample ディレクトリに戻り、以下のコマンドを実行して csproj 内の依存関係を復元します。
cd ~/SqlServerColumnstoreSample dotnet restore
完了したら、ビルド実行を行います。
dotnet runDocker Compose で実行する場合は、以下のコマンドを実行してください。
docker-compose run -w /src/SqlServerColumnstoreSample --rm app dotnet restore docker-compose run -w /src/SqlServerColumnstoreSample --rm app dotnet runおめでとうございます。カラムストアインデックスを使って C# アプリを高速化しました!
おわりに
以上で、「macOS上でSQL Serverを使用してC#アプリを作成する」は終了です。Build an app using SQL Server には、他言語での SQL Server アプリを作成するチュートリアルがあります。ぜひ、他の言語でも試してみてください。
- 投稿日:2020-05-19T00:56:42+09:00
Xamarin Android 各要素の高さが定まっていないGridViewのCustomRenderer作成
前書き
GridViewのカスタムレンダラー化は過去記事をご覧ください。
Xamarin Android GridViewのカスタムレンダラーの作成今回は上記の記事で既にカスタムレンダラー化されたGridViewがある前提で、
そのGridViewを各要素の高さが定まっていないGridViewにカスタマイズしていきます。やりたいこと
具体的に私の場合は、人(要素)に対して複数件のデータが紐づいているような一覧表示をしようとしました。
【完成系】
実装方法
GridViewは高さの違う要素がある状態でスクロールするとGridViewが消えるようなので(実装中発見しました)
要素の高さを、行の中で一番高いものにリサイズする実装を選びました。実装
※私のnamespaceやプロジェクト名は自分のものに置き換えてください
PLC側
using System.Runtime.CompilerServices; using Xamarin.Forms; [assembly: InternalsVisibleTo("MyProject.Android")] namespace MyProject.Views.Renderer { public class PCLCustomGridViewRenderer : ItemsView { } }XamlからPCLCustomGridViewRendererを呼び出します
<StackLayout xmlns:renderer="clr-namespace:MyProject.Views.Renderer"> <renderer:PCLCustomGridViewRenderer ItemsSource="{Binding Items}"/> </StackLayout>バインドしてるItemsの中身です。
IList<Item> Items; public class Item { public string Name { get; set; } public ObservableCollection<ItemDetail> Details { get; set; } } public class ItemDetail { public string Text { get; set; } }Xamarin.Android側
AndroidCustomGridViewRenderer.cs
using Android.Content; using Android.Support.V7.View; using Android.Widget; using MyProject.Droid.Renderer; using MyProject.Droid.Views; using MyProject.Droid.Views.Adapter; using MyProject.Views.Renderer; using System.ComponentModel; using Xamarin.Forms; using Xamarin.Forms.Platform.Android; //互いに参照できるよう定義 [assembly: ExportRenderer(typeof(PCLCustomGridViewRenderer), typeof(AndroidCustomGridViewRenderer))] namespace MyProject.Droid.Renderer { public class AndroidCustomGridViewRenderer : ViewRenderer<PCLCustomGridViewRenderer, GridView> { private AndroidCustomGridViewAdapter adapter; public AndroidCustomGridViewRenderer(Context context) : base(context) { } // 生成時一度だけ呼ばれるイベント protected override void OnElementChanged(ElementChangedEventArgs<PCLCustomGridViewRenderer> e) { base.OnElementChanged(e); if (e.OldElement != null) { if (adapter != null) adapter.Element = null; } if (e.NewElement != null) { if (Control == null) { // Adpterの生成 adapter = new AndroidCustomGridViewAdapter(Context); // GridViewの生成 var gridView = new CustomGridView(new ContextThemeWrapper(Context, Resource.Style.VerticalScrollbarRecyclerView)); // GridViewのパラメータを設定(親サイズに追従する設定) gridView.LayoutParameters = new LayoutParams(LayoutParams.MatchParent, LayoutParams.MatchParent); // カラム数 gridView.NumColumns = 3; // Viewに対しAdpterを設定 gridView.Adapter = adapter; //多分コントロールを実際に生成 SetNativeControl(gridView); } if (adapter != null) // NewElementにはIList<Person>が入ってきます // ElementはAdpterクラスが私が定義しました。 adapter.Element = e.NewElement; } } protected override void OnElementPropertyChanged(object sender, PropertyChangedEventArgs e) { base.OnElementPropertyChanged(sender, e); if (e.PropertyName == ItemsView.ItemsSourceProperty.PropertyName) { adapter?.UpdateItems(); } } protected override void Dispose(bool disposing) { base.Dispose(disposing); adapter.Dispose(); } } }CustomGridView.cs
描画前に各要素に高さの測定を指示します
using Android.Content; using Android.Util; using Android.Widget; using MyProject.Droid.Views.Adapter; namespace MyProject.Droid.Views { class CustomGridView : GridView { public CustomGridView(Context context) : base(context) { } public CustomGridView(Context context, IAttributeSet attrs) : base(context, attrs) { } public CustomGridView(Context context, IAttributeSet attrs, int defStyle) : base(context, attrs, defStyle) { } protected override void OnLayout(bool changed, int l, int t, int r, int b) { // グリッドビューの表示時に、各要素の高さを図る AndroidCustomGridViewAdapter adapter = (AndroidCustomGridViewAdapter)Adapter; // 列数と、要素数の設定 CustomGridViewLinearLayout.initItemLayout(NumColumns, adapter.Count); int columnWidth = MeasuredWidth / NumColumns; // 各要素の高さを測定 adapter.measureItems(columnWidth); base.OnLayout(changed, l, t, r, b); } } }AndroidCustomGridViewAdapter.cs
using System.Collections.ObjectModel; using System.Linq; using Android.Content; using Android.Views; using Android.Widget; using MyProject.Models; using MyProject.Views.Renderer; namespace MyProject.Droid.Views.Adapter { class AndroidCustomGridViewAdapter : BaseAdapter { private PCLCustomGridViewRenderer element; public PCLCustomGridViewRenderer Element { get => element; set { element = value; UpdateItems(); } } Context context; private ObservableCollection<Item> viewModels = new ObservableCollection<Item>(); public AndroidCustomGridViewAdapter(Context c) { context = c; } public override int Count { get { return viewModels.Count(); } } public override Java.Lang.Object GetItem(int position) { return null; } public override long GetItemId(int position) { return 0; } public void UpdateItems() { viewModels.Clear(); if (Element?.ItemsSource != null) { foreach (var item in Element.ItemsSource) { if (item is Item model) { viewModels.Add(model); } } } NotifyDataSetChanged(); } public override Android.Views.View GetView(int position, Android.Views.View convertView, ViewGroup parent) { var view = (CustomGridViewLinearLayout)LayoutInflater.From(parent.Context).Inflate(Resource.Layout.custom_gridview_item, parent, false); view.setPosition(position); // 要素の作成 SetUp(view, viewModels[position]); return view; } private void SetUp(Android.Views.View view, Item model) { var name = view.FindViewById<TextView>(Resource.Id.name); var details = view.FindViewById<LinearLayout>(Resource.Id.details); name.Text = model.Name; details.RemoveAllViews(); foreach (var detail in model.Details) { var textView = new TextView(view.Context); textView.Text = detail.Text; details.AddView(textView); } } /// <summary> /// 描画の前に各要素の高さを図ります /// </summary> /// <param name="columnWidth">列一つ分の幅</param> public void measureItems(int columnWidth) { LayoutInflater inflater = (LayoutInflater)context.GetSystemService(Context.LayoutInflaterService); CustomGridViewLinearLayout itemView = (CustomGridViewLinearLayout)inflater.Inflate(Resource.Layout.custom_gridview_item, null); int widthMeasureSpec = Android.Views.View.MeasureSpec.MakeMeasureSpec(columnWidth, MeasureSpecMode.Exactly); int heightMeasureSpec = Android.Views.View.MeasureSpec.MakeMeasureSpec(0, MeasureSpecMode.Unspecified); for (int index = 0; index < viewModels.Count; index++) { Item item = viewModels[index]; // 測定用のViewを設定 itemView.setPosition(index); SetUp(itemView, item); // 強制的にViewのOnMeasureイベントを発火 itemView.RequestLayout(); itemView.Measure(widthMeasureSpec, heightMeasureSpec); } } }CustomGridViewLinearLayout.cs
GridView内の1要素
using Android.Content; using Android.Util; using Android.Widget; namespace MyProject.Droid.Views { class CustomGridViewLinearLayout : LinearLayout { private static int[] maxRowHeight; private static int numColumns; private int position; public CustomGridViewLinearLayout(Context context) : base(context) { } public CustomGridViewLinearLayout(Context context, IAttributeSet attrs) : base(context, attrs) { } public void setPosition(int position) { this.position = position; } public static void initItemLayout(int numColumns, int itemCount) { CustomGridViewLinearLayout.numColumns = numColumns; maxRowHeight = new int[itemCount]; } /// <summary> /// 自分の高さを計測、同じ行に自分より高い要素があれば、高さを上書き /// </summary> /// <param name="widthMeasureSpec"></param> /// <param name="heightMeasureSpec"></param> protected override void OnMeasure(int widthMeasureSpec, int heightMeasureSpec) { base.OnMeasure(widthMeasureSpec, heightMeasureSpec); if (numColumns <= 1 || maxRowHeight == null || maxRowHeight.Length == 0) { return; } int rowIndex = position / numColumns; int measuredHeight = MeasuredHeight; if (measuredHeight > maxRowHeight[rowIndex]) { maxRowHeight[rowIndex] = measuredHeight; } SetMeasuredDimension(MeasuredWidth, maxRowHeight[rowIndex]); } } }custom_gridview_item.xml
Resources/layoutフォルダに配置してください
<MyProject.Droid.Views.CustomGridViewLinearLayout xmlns:android="http://schemas.android.com/apk/res/android" android:background="@drawable/gridview_item_frame" android:padding="7dp" android:orientation="vertical" android:layout_width="match_parent" android:layout_height="match_parent"> <TextView android:id="@+id/name" android:singleLine="true" android:text="aaaa" android:textSize="18dp" android:textColor="#464646" android:layout_width="match_parent" android:layout_height="wrap_content"/> <LinearLayout android:id="@+id/details" android:layout_marginTop="10dp" android:orientation="vertical" android:layout_width="match_parent" android:layout_height="wrap_content" /> </MyProject.Droid.Views.CustomGridViewLinearLayout>gridview_item_frame.xml
Resources/drawbleフォルダに配置してください。
要素に枠を追加します。<shape xmlns:android="http://schemas.android.com/apk/res/android" android:shape="rectangle"> <stroke android:width="1dp" android:color="#e8d689"/> <solid android:color="@android:color/white" /> <corners android:radius="1dp" /> </shape>custom_gridview_item_detail.xml
Resources/layoutフォルダに配置してください
<?xml version="1.0" encoding="utf-8"?> <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" android:orientation="vertical" android:layout_width="match_parent" android:layout_height="match_parent"> <TextView android:id="@+id/text" android:text="ああああああああああああああああ" android:layout_centerHorizontal="true" android:layout_centerVertical="true" android:layout_width="wrap_content" android:layout_height="wrap_content"/> </LinearLayout>完成
ビルドしてみてください。
コード量が多くなってしまいましたが、ジェネリクスを使えば、もっと汎用的なクラスにできると思います。