20200227のC#に関する記事は9件です。

WPF フォルダ選択ダイアログ

■概要

フォルダ選択ダイアログ。
FolderBrowse0.png

■環境

  • Windows 10
  • Visual Studio 2019
  • .NET Framework 4.8

■準備

NuGetでMicrosoft.WindowsAPICodePack-Shellをインストール。

FolderBrowse1.png

■コード例

MainWindow.xaml
<Window x:Class="FolderBrowse1.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
        xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
        xmlns:local="clr-namespace:FolderBrowse1"
        mc:Ignorable="d"
        Title="MainWindow" Height="450" Width="800">
    <Grid>
        <Button Content="押せ" HorizontalAlignment="Left" Height="67" 
            Margin="235,35,0,0" VerticalAlignment="Top" Width="109" 
            Click="Button_Click"/>
    </Grid>
</Window>
MainWindow.xaml.cs
using System.Windows;
using MSAPI = Microsoft.WindowsAPICodePack;

namespace FolderBrowse1
{
    /// <summary>
    /// MainWindow.xaml の相互作用ロジック
    /// </summary>
    public partial class MainWindow : Window
    {
        /// <summary>
        /// コンストラクタ
        /// </summary>
        public MainWindow()
        {
            InitializeComponent();
        }

        /// <summary>
        /// ボタンクリック時の処理
        /// </summary>
        /// <param name="sender"></param>
        /// <param name="e"></param>
        private void Button_Click(object sender, RoutedEventArgs e)
        {
            var dlg = new MSAPI::Dialogs.CommonOpenFileDialog();

            // フォルダ選択ダイアログ(falseにするとファイル選択ダイアログ)
            dlg.IsFolderPicker = true;
            // タイトル
            dlg.Title = "フォルダを選択してください";
            // 初期ディレクトリ
            dlg.InitialDirectory = @"D:\Work";

            if (dlg.ShowDialog() == MSAPI::Dialogs.CommonFileDialogResult.Ok)
            {
                MessageBox.Show($"{dlg.FileName}が選択されました。");
            }
        }
    }
}

■実行

FolderBrowse2.png

FolderBrowse3.png

FolderBrowse4.png

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

C#の単体テストは「Chaining Assertion」が便利

はじめに

C#の単体テストで期待値の確認は「Chaining Assertion」というライブラリがとても便利でお勧めです。
Chaining Assertion の公式サイトはこちら

他のテスト記述ライブラリとの比較

値が等しいことの確認

テスト記述ライブラリで有名なものとして「Fluent Assertions」があります。
Fluent Assertions の公式サイトはこちら
それを用いてテストする場合、以下のように書きます。
(標準のAssertクラスよりも書きやすいので、こちらのライブラリもとても素晴らしいと思います)

// Fluent Assertions で文字列が空文字であることを確認する例
theString.Should().Be(string.Empty);

これに対して、Chaining Assertion は、Should().Be() でなく Is() だけで確認できるため、よりシンプルに記述できます。

// Chaining Assertion で文字列が空文字であることを確認する例
theString.Is(string.Empty);

値が等しい以外(値が範囲内かなど)の確認

Fluent Assertions は期待値を確認するためのメソッド(BeNull、HaveLengthなど)をたくさん提供しています。これは一見すごく多機能で便利なのですが、実際にテストコードを書くときには適切なメソッドを調べることになるので意外と手間がかかります。
例えば、値が0以上10以下であることを確認しようと思った時に、範囲指定のメソッドがあるので、以下のように記述します。

// Fluent Assertions で対象の値が0以上10以下であることを確認する例
theInt.Should().BeInRange(0, 10);

Fluent Assertions の数値型の確認メソッドはこちら

それに対して Chaining Assertion は、シンプルに Is メソッドのみで期待値を確認します。Is の引数にラムダ式を書くことで、Is だけでどんな期待値も確認できます。以下のような感じです。

// Chaining Assertion で対象の値が0以上10以下であることを確認する例
theInt.Is(value => value >= 0 && value <= 10);

C#開発者は、ラムダ式を書き慣れているので、確認したい内容に合わせてメソッドを探すよりも、全部ラムダ式で書く方が楽だと思います。
(これに関しては個人の感想ですが、私のチームで両方のライブラリをそれぞれ一定期間使ってみたところ、いずれのメンバーも Chaining Assertion の書き方の方が気に入りました)

コレクションの確認

コレクションに関しては、Fluent Assertions はコレクション用のメソッドもいっぱいあります。適切なメソッドを探すのが少し手間です。Chaining Assertion 作者の河合さんが以下の記事で解説されているように、コレクションは慣れ親しんだ Linq to Objects で結果まで絞って True/Falseを判定する方が楽だと思います。
neue cc - メソッドチェーン形式のテスト記述ライブラリ

// Fluent Assertions でコレクションの要素に3より大きい値があることを確認する例
// (何十個もあるコレクションのメソッドの中から適したメソッドを使う)
collection.Should().HaveCountGreaterThan(3);

// Chaining Assertion でコレクションの要素に3より大きい値があることを確認する例
// (慣れ親しんだ Linq to Objects を使う)
collection.Any(c => c > 3).Is(true);

その他の便利機能

Chaining Assertion の便利機能を紹介します。

プライベートメンバーへのアクセスが容易

期待値を確認するときに、privateメンバーにアクセスしたい場合があります。
その場合、PrivateObjectクラスを利用してアクセスします。ただ、PrivateObjectの実装は、PrivateObjectのインスタンスを作ってからそれを通してアクセスする必要があり、少し面倒です。
PrivateObjectクラスの実装例

Chaining Assertion なら、privateメンバーへのアクセスを簡潔に実装できます。
具体的には、object型の拡張メソッドであるAsDynamic()メソッドを利用することでprivateメンバーにアクセスできます。
例を以下に示します。

    // privateメンバーを持つクラス
    public class MyClass
    {
        private string PrivateMethod(int number)
        {
            return number;
        }
    }

    [TestClass]
    public class UnitTest
    {
        [TestMethod]
        public void TestMethod()
        {
            var myClass = new MyClass();
            // AsDynamic()の戻り値は dynamic型
            // AsDynamic()に続けて呼び出したいメンバーを書くことでリフレクションで実行される
            int number = myClass.AsDynamic().PrivateMethod(3);
            number.Is(3);
        }
    }

再帰的な公開フィールド・プロパティの確認

あるインスタンスを複製するメソッドを作った場合、その複製されたインスタンスのフィールド値および、そのインスタンスが保持しているオブジェクトが複製元と一致するか確認したい場合があります。
Chaining Assertion なら IsStructualEqualメソッドを呼び出すだけで、複製したオブジェクトのフィールド値およびそれが保持するオブジェクトについて、参照をたどって再帰的に確認できます(publicなフィールドまたはプロパティを確認します)。
以下に例を示します。

    // テスト対象クラス
    public class MyClass
    {
        public string Name
        {
            get;
            set;
        }

        public MyClass Child
        {
            get;
            set;
        }

        public MyClass Clone()
        {
            // 複製する処理
        }
    }

    [TestClass]
    public class UnitTest
    {
        [TestMethod]
        public void TestMethod()
        {
            // テスト対象クラスを作成する
            var myClass = new MyClass()
            {
                Name = "Test",
                Child = new MyClass(),
            };
            // 複製する
            var clone = myClass.Clone();

            // 参照しているChildのフィールド値も含めて一致することを確認
            clone.IsStructuralEqual(myClass);
        }
    }

ただし、相互参照している場合は無限ループとなるため注意が必要です。
そのあたりの挙動を変更することも可能です。Chaining Assertion はインストールするとDLLを参照するのでなく、ソースコードを1ファイル追加するだけなので、そのソースコードを変更することで、自前で挙動をカスタマイズすることもできます。

まとめ

Chaining Assertion は便利なのでお勧めです。

私は上記手法を用いて こちらのツール を作っています。

Twitterでも開発に役立つ情報を発信しています → @kojimadev

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

WinFormsのListView.SelectedIndicesが遅い問題を回避する

仮想モードにしたListViewでも、SelectedIndicesプロパティ(ListView.SelectedIndexCollectionクラス)で非常に時間がかかるケースがあります。
(この記事では面倒なのでどれくらい時間がかかるかは述べません)

遅くならないケース

  • SelectedIndices.Count
    内部ではLVM_GETSELECTEDCOUNTを使って実装されているので高速に取得できます。
  • SelectedIndices[0]
    1件目であれば、列挙を打ち切るため遅くはなりません。

全選択/全選択解除を早くする

以下のように、ループでSelectedIndices.Add/Removeでしていくと非常に時間がかかってしまいます。

NG
for (var i = 0; i < listView1.Items.Count; i++)
{
    listView1.SelectedIndices.Add(i);
}

ListBoxの全項目を高速に選択すると同様に、P/Invokeで全選択するようにします。

こんなのを定義して、

const int LVM_FIRST = 0x1000;
const int LVM_SETITEMSTATE = LVM_FIRST + 43;

[StructLayout(LayoutKind.Sequential, CharSet = CharSet.Auto)]
public struct LVITEM
{
    public int mask;
    public int iItem;
    public int iSubItem;
    public int state;
    public int stateMask;
    [MarshalAs(UnmanagedType.LPTStr)]
    public string pszText;
    public int cchTextMax;
    public int iImage;
    public IntPtr lParam;
    public int iIndent;
    public int iGroupId;
    public int cColumns;
    public IntPtr puColumns;
};
private const int LVIS_SELECTED = 2;

public static void SelectAll(ListView control, bool select)
{
    var lvItem = new LVITEM
    {
        stateMask = LVIS_SELECTED,
        state = select ? LVIS_SELECTED : 0
    };
    SendMessage(new HandleRef(control, control.Handle), LVM_SETITEMSTATE, new UIntPtr(unchecked((uint)-1)), ref lvItem);
}

[DllImport("user32.dll", CharSet = CharSet.Auto)]
internal static extern IntPtr SendMessage(HandleRef hWnd, int msg, UIntPtr wParam, ref LVITEM lvi);

こう使います。

    // 全選択
    SelectAll(listview1, true);

    // 選択解除
    SelectAll(listview1, false);

選択行の取得を早くする

SelectedIndicesに対して列挙すると、内部では1件目から全部舐めてリストを作ることになるので、非常に遅くなります。
(foreachで回さなくても、CopyToメソッドなども含む)
大きなインデックスでインデックスアクセスしても同様にその位置まで1件進めるのに時間がかかります。

NG
foreach (int i in listView1.SelectedIndices)
{
    // iを使用した処理
}

そのため、VirtualItemsSelectionRangeChangedItemSelectionChangedイベントで選択行を保持しておき、選択行が必要なときはそれを参照するようにします。

private bool[] _selected = new bool[/*listView1.VirtualListSize分のサイズ*/];
private void listView1_VirtualItemsSelectionRangeChanged(object sender, ListViewVirtualItemsSelectionRangeChangedEventArgs e)
{
    for (var i = e.StartIndex;  i <= e.EndIndex; i++)
    {
        _selected[i] = e.IsSelected;
    }
}
private void listView1_ItemSelectionChanged(object sender, ListViewItemSelectionChangedEventArgs e)
{
    _selected[e.ItemIndex] = e.IsSelected;
}

他の方法として、

  • C#を諦めてC++/CLIで同等の処理を実装する
  • 別途ネイティブDLLを作成してそれをP/Invokeで呼び出す

などもあります。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

今更ながらWPFに置き換えてみる(4)

多分WPFの開発はXAMLでがっちり記述するのが大前提なんだろーな、と思ってはいるんですが、なんかスカスカ感があってついデザイナ中心で作ってしまいがちです。皆さんはどうなんでしょうか。

ということで、
・画面構成はほぼ今のFORMSのVBアプリと同様。
・時報機能あり。時間になるとこんな感じのNOTIFYが表示される

無題.png

・タスクバー内のNotificationも今と同様のバリエーション
・NotifyIconクリックでメイン画面をShowする際に、Screenのエッジにスナップ

新たにこんなことができるかを確認しつつ可能であれば実装
・NotifyIconをデスクトップにドラッグすることでNotifyIconを消去しアプリのタスク専用の小Windowを表示。
・同様に逆に小WindowをタスクバーにドラッグすることでNotifyIcon化
(これイベントトラップ細工しないと難しそう)
・メインのウインドウのShow/Hideの際のトランジションをなんか工夫

見た目重視。
プロセスのアイコン化ってそれほど特殊な考え方じゃないハズ。UNIXとかそうなんですよねよく知らないけど。
NeXTの適当にまねて作ったこんな感じのとか↓。
コメント 2020-02-27 210952^2.png

Macで最初に手に入れたThinkCって言語環境についてたリファレンスアプリがいい感じにやってたのを思い出して、あれを目標に。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

【Unity】楽曲に合わせて〇〇するスクリプト

こんにちはっ?八ツ橋まろんです

個人バーチャルYouTuberとして、UnityのTimeline機能を使って、3Dモデルを使ったオリジナルのミュージックビデオを制作しています。

楽曲のリズムに合わせてパーティクルを出したり演出を入れたりしているのですが、Timelineの時間軸はフレーム単位(60フレーム/秒)なので、「100bpmの楽曲だから60/100 = 0.6秒 = 36フレームごとにパーティクルをEnableにして、、、」とか、「155bpmだと割り切れないから〇回に1回だけ1フレーム減らして、、、」なんて作業は、正直しんどいのでやっていられません。

なので、『最初の1回だけ指定したら残りは同じ時間サイクルで動作をしてくれるスクリプト』を書きました?(小難しくない簡単なスクリプトです)

使用例:一定間隔でハートが鼓動を打つ
qiia.gif

BpmAction.cs
using System.Collections;
using UnityEngine;


public class BpmAction : MonoBehaviour
{
    public int bpm;

    enum Coef
    {
        quarter,
        half,
        x1,
        x2
    }

    [SerializeField]
    Coef coef;

    [Header("Events")]
    public OnBPM onBPM;

    float BpmCoef()
    {
        switch (coef)
        {
            case Coef.quarter:
                return 0.25f;
            case Coef.half:
                return 0.5f;
            case Coef.x1:
                return 1f;
            case Coef.x2:
                return 2f;
        }
        return 1f;
    }

    [SerializeField]
    bool IsBpmAction;
    float time;

    private void Update()
    {
        if (IsBpmAction)
        {
            time += Time.deltaTime;
            if(time >= 60f / (bpm * BpmCoef()))
            {
                time -= 60f / (bpm * BpmCoef());
                Action();
            }
        }
    }

    void Action()
    {
        onBPM.Invoke();
    }
    [System.Serializable]
    public class OnBPM : UnityEngine.Events.UnityEvent
    {

    }
    public void BpmActionStart()
    {
        IsBpmAction = true;
        Action();
    }
    public void BpmActionStop()
    {
        IsBpmAction = false;
        time = 0;
    }
    public void SetNewBpm(int i)
    {
        BpmActionStop();
        bpm = i;
    }
}

【ポイント】
以下の記述によって、uGUIのボタンのように、イベント登録ができるのでとっても便利です。覚えておきましょう。私も今回初めて知りました。(下の画像の赤枠部分が追加される)

冒頭のハートの例では、ハートのAnimatorにSetTrigger("Beat")をして鼓動させています。

    [Header("Events")]
    public OnBPM onBPM;

    [System.Serializable]
    public class OnBPM : UnityEngine.Events.UnityEvent
    {

    }

無題 (復元済み).png

また、時間の計測は、コルーチンを使わず、Update関数内でTime.deltatimeを毎フレーム足しています。

通常、〇〇時間後に起動する処理ではコルーチンをよく使いますが、更新はフレーム毎にしか行われないので、例えば「1.00秒の設定に対して1.01秒経った→1.00秒を越えているのでカウントを0にする」という処理では、毎回0.01秒遅いアクションになり、積もり積もって時間差が目立ってしまいます。そのため、「Updateで毎回Time.deltatimeを足す→1.01秒になったのでカウントを1.00秒引いて0.01秒から再スタート」とすることで正確な時間計測をしています。
(コルーチンでのやり方あったら教えてほしいです。よくわからんかった。)

【使い方】
・BPMに楽曲のBPMを入れてください。

・Unity2019以降:Timeline上にSignal Trackを追加し、最初の1回目の位置にSignalを置いてBpmActionStart()関数を呼んでやればOKです?終わるときはBpmActionStop()関数を呼んでください。

・Unity2018以前:TimelineにSignal機能がないため、追加でAnimatorを付けて、TimelineにAnimation Trackを追加して、IsBpmActionをtrueにすればOKです?終わるときはfalseにしてください。

・Coefは係数です。x1で毎ビート毎に、x2で2倍、halfで2回に1回、quarterで4回に1回です。

以上、「楽曲に合わせて〇〇するスクリプト」でしたっ?またね

八ツ橋まろん

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

WPFのコントロールにDirectX12で描画する

モチベーション

モーショングラフィックスエンジン的なものを作りたい。
グラフィックスの描画はDXRを見越してDirectX12が使いたい。
でも、周りのUIはグラフィックスとは分離して、MVC的なフレームワークで作りたい。

Unityでいいじゃんって話なのですが…

やりたいこと

WPFで色々なパラメータやリソースを表示編集できるWindowの中に、DirextXが描画するコントロールを置きたい。
コメント 2020-02-27 144421.png

ポリ一枚表示しただけですし、スライダーで動くわけでもなければリサイズにも対応していませんが、
一応上記画像のようにできました。

D3DImage/D3D11Image(今回は不採用)

WPFにはD3DImage/D3D11Imageというクラスが用意されており、それぞれDirextX9/11向けのRenderTargetを提供しています。
WPFDXInterop

DirectX12向けのクラスは提供されていませんが、DirectX11-12間でテクスチャリソースの共有が可能です。
DirectX11のWPFDXInteropでは、WPFControlからIDXGIResourceが手に入るので、それからID3D11Texture2Dを取得し、RenderTargetViewを作成し描画します。
DirectX12の場合も同様にIDXGIResourceからID3D12Resource1を取得してRenderTargetViewを作成して描画すればいいはずです。
(DirectX12ではTextureやConstantBufferなどのインターフェイスとしての垣根がなくなり、ID3D12Resource1に統一されました。)

しかし、いざ実装してみようとすると、SwapChain使わない場合のやり方などがよくわからなかったので、確実なHwndHostを使ってみました。
ID3D12CommandQueue::ExecuteCommandListsの後、IDXGISwapChain::Presentを呼ばずに、描画完了も待たずにリターンすればいいのでしょうか?WPFコントロールから渡されるIDXGIResourceはバックバッファなんですかね?とすると、描き切れなかったら半端なRTが表示されちゃう???

HwndHost(WPF Control)とは

DirectX12のサンプルなどでは生Win32でGUIを生成して、ルートのWindowに対して描画をしています。
Win32のGUIというのは、以下のようなもので前半部がWindowの初期化、後半部がいわゆるUpdateやイベント(メッセージ)処理です。
HWNDという変数が出てきますが、これはFormやWPFでいうWindow/FormやControlに相当するもので文字通りのウインドウからボタンまでWindowとして扱い、HWNDで管理します。
DirectXにHWNDを指定して描画させる場合、このWindowの矩形領域内に描画することになります。

main.cpp
int WINAPI WinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance, PSTR lpCmdLine, int nCmdShow)
{
    WNDCLASS winc; // WNDCLASSの設定
    HWND hwnd = CreateWindow(
        TEXT("MYCLASSNAME"), TEXT("Title"),
        WS_OVERLAPPEDWINDOW | WS_VISIBLE,
        CW_USEDEFAULT, CW_USEDEFAULT,
        CW_USEDEFAULT, CW_USEDEFAULT,
        NULL, NULL, hInstance, NULL
    );

    MSG msg;
    while (GetMessage(&msg, NULL, 0, 0))
    {
        DispatchMessage(&msg);
        theApp.Render();
    }
    return msg.wParam;
}

それで、HwndHostはこのWINAPIの枠を提供してくれます。
実質的にはWindowの矩形領域を提供してくれる&HWNDを保持管理してくれます。

class MyControl : HwndHost
{
    protected virtual HandleRef BuildWindowCore(HandleRef hwndParent)
    {
        // 初期化処理(ただし、この時点ではまだ矩形情報が取れない!)
        // CreateWindowとかして、帰ってきたHWNDを返す
    }
    protected virtual IntPtr WndProc(IntPtr hwnd, int msg, IntPtr wParam, IntPtr lParam, ref bool handled)
    {
        // Win32の後半のメッセージループの部分
        // DirectXのUpdate(Render)処理はここで
    }
    protected virtual void DestroyWindowCore(HandleRef hwnd)
    {
        // 終了処理
    }
}

ここにうまいことDirectX12の初期化処理とUpdate(Render)処理を挟みます。
この方法の最大の利点は、Win32で作る場合とほとんど変わらない…というかWin32との互換性維持のための機能という点です。
DirextXはそもそもゲームを作るためのAPIなので外部GUIは不要でWindowいっぱいに描けばいいので、手に入るサンプルやノウハウも大抵の場合は生Win32のルートWindowに描いています。それらを参考にできます。

ということで、このHwndHostとWin32とDirectX12合体させてみたのがこちらになります。
DirectX12の処理は「DirectX12 Programming Vol.1」の付録のサンプルコードを流用させていただきました。

DX.cs
class DX : HwndHost
{
    [Flags] enum WindowStyle : int { /* 省略 */ }

    IntPtr app = IntPtr.Zero;

    protected override HandleRef BuildWindowCore(HandleRef hwndParent)
    {
        // Win32のWindowの初期化
        IntPtr hwnd = CreateWindowEx(
            0, "STATIC", "",
            WindowStyle.WS_CHILD | WindowStyle.WS_VISIBLE,
            0, 0,
            (int)ActualWidth, (int)ActualHeight,
            hwndParent.Handle,
            (IntPtr)WindowStyle.HOST_ID,
            IntPtr.Zero, 0);
        return new HandleRef(this, hwnd);
    }

    protected override IntPtr WndProc(IntPtr hwnd, int msg, IntPtr wParam, IntPtr lParam, ref bool handled)
    {
        if (app == IntPtr.Zero)
        {
            // DirectX12の初期化
            // BuildWindowCoreでInitしたかったが、は矩形が0のままなのでDepthBufferが作れない。
            // 本当はリサイズも考慮してデバイスのInitとRenderTarger/DepthBufferの生成を分けるべき。
            app = Init(hwnd, (int)ActualWidth, (int)ActualHeight);
        }

        // DirectX12の描画(のリクエスト)処理
        Render(app);

        handled = false;
        return IntPtr.Zero;
    }

    protected override void DestroyWindowCore(HandleRef hwnd)
    {
        // Win32のWindowとDirectX12の終了処理
        DestroyWindow(hwnd.Handle);
        Dispose(app);
    }

    [DllImport("user32.dll")]
    static extern IntPtr CreateWindowEx( /* 省略 */ );
    [DllImport("user32.dll")]
    static extern bool DestroyWindow(IntPtr hwnd);

    [DllImport("02_SimpleTriangle.dll")]
    static extern IntPtr Init(IntPtr hwnd, int width, int height);
    [DllImport("02_SimpleTriangle.dll")]
    static extern void Render(IntPtr app);
    [DllImport("02_SimpleTriangle.dll")]
    static extern IntPtr Dispose(IntPtr app);
}
Export.cpp
TriangleApp* Init(HWND hwnd, int width, int height)
{
    auto app = new TriangleApp(); // TriangleApp.h
    app->Initialize(hwnd, width, height);
    return app;
}

void Render(TriangleApp* app)
{
    app->Render();
}

void Dispose(TriangleApp* app)
{
    app->Cleanup();
    delete app;
}
MainWindow.xaml
<Window> <!-- 属性は省略 -->
    <Grid>
        <local:DX Margin="18,43,342,122"></local:DX>
        <WrapPanel Margin="527,101,53,281">
            <TextBlock Text="Hogehoge"></TextBlock>
        </WrapPanel>
        <Slider Margin="517,149,39,231"></Slider>
    </Grid>
</Window>

DLLImportするときはImportするDLLが参照しているDLLもexeのディレクトリに並べないとDllNotFoundExceptionが出ます。
この例では、02_SimpleTriangle.dlldxcompiler.dllなどを参照していてハマりました。

全文は こちら(Github)

技術領域問題

WPFのコントロールは一枚のDirectX9で描かれています。そこに無理やりWin32のウインドウを乗っけてそこにDirextX12で描画している訳です。
なのでWPFコントロールでサンドイッチすることができません。

詳しくは 技術領域の概要(MSDN)

余談

DirectX12の情報…少なすぎ!!
最近のMSの動向としてはDirectXを触るのは本当に限られた場合だけで、基本的にはUnityとか使ってね!君たちはDirectXなんて知らなくていいよ!ってスタンスですし、いまだにDirectX9が現役(?)ですからね。ゲームの専門学校などでもいまだにDirectX9と聞きます。確かにシンプルで初学向けなのかもしれませんが。
MSDNは情報「量」だけは結構ありますし、「DirectX12 Programming Vol.1」のおかげで基本的な使い方は理解できましたが、少し外れたことを、応用したいと思うと似たような例が見つかりませんね。

参考

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

StreamingAssetsでTextureをロードする方法

はじめに

意外と、これだけのシンプルな内容の記事がなかったので端的に書いてみました。

対象

・unityのC#がわかる
・Resouces使いたくない
・コルーチンがわかる
・Actionがかわる
・WWW使いたくない

本題

using UnityEngine.Networking;

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

        private Texture textureTest;
        protected string path;

        public void Start() {
//StreamingAssets内のTestフォルダのAというjpeg画像のパス指定
      SetPath(Test/A.jpg);
           StartCoroutine (DownloadTexture (r => textureTest = r));
        }

//StreamingAssetsフォルダー内のパスの指定
        public void SetPath (string url) {
            path = url;
        }

        public IEnumerator DownloadTexture (Action<Texture> callback) {
//StreamingAssetsまでのパスと内側のパスをつなげる
            var url = System.IO.Path.Combine (Application.streamingAssetsPath, path);
            url = "file://" + url;

            using (UnityWebRequest www = UnityWebRequestTexture.GetTexture (url)) {
                yield return www.SendWebRequest ();
                if (www.isNetworkError || www.isHttpError) {
                    Debug.Log (www.error);
                    yield break;
                }
                var myTexture = ((DownloadHandlerTexture) www.downloadHandler).texture;
//コルーチンは返り値を持てないので、Actionで返す
                callback (myTexture);
            };
        }

解説

SetPath(Test/A.jpg);

この引数の部分を自分のフォルダーに変更してください。

最後に

StreamingAssetsはなんぞや
どういう利点が?
等は他に良記事が多数なのでググってみてください。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Google Calendar API #2

前回の記事
Google Calendar API #1

環境

IDE:VisualStudio2019
アプリケーション:コンソールアプリ
フレームワーク:.NET Core 3.1

カレンダーに予定を追加

前回は予定取得を試してみました。
今回はカレンダーの予定追加/更新を行ってみます。

まずわかりやすいように自分のカレンダーの直近の予定を空にします。
image.png

予定を追加するコードを記述します。
以下公式にサンプルがありました。
Events: insert

以下のようなスケジュール追加メソッドを用意

        /// <summary>
        /// カレンダーイベントを追加
        /// </summary>
        /// <param name="calendarId">カレンダーID</param>
        public void InsertEvent(string calendarId)
        {
            var newEvent = new Event()
            {
                Summary = "Google I/O 2020",
                Location = "神奈川県横浜市",
                Description = "テスト備考",
                Start = new EventDateTime()
                {
                    DateTime = DateTime.Parse("2020/02/28 9:00:00"),
                    TimeZone = "Asia/Tokyo",
                },
                End = new EventDateTime()
                {
                    DateTime = DateTime.Parse("2020/02/28 17:00:00"),
                    TimeZone = "Asia/Tokyo",
                },
                //Recurrence = new string[] { "RRULE:FREQ=DAILY;COUNT=2" },
                //Attendees = new EventAttendee[] {
                //    new EventAttendee() { Email = "lpage@example.com" },
                //    new EventAttendee() { Email = "sbrin@example.com" },
                //},
                //Reminders = new Event.RemindersData()
                //{
                //    UseDefault = false,
                //    Overrides = new EventReminder[] {
                //        new EventReminder() { Method = "email", Minutes = 24 * 60 },
                //        new EventReminder() { Method = "sms", Minutes = 10 },
                //    }
                //}
            };

            var request = this.Serive.Events.Insert(newEvent, calendarId);
            var createdEvent = request.Execute();
            Console.WriteLine("Event created: {0}", createdEvent.HtmlLink);
        }

結果
image.png

サンプル全文

規定クラスを作りました。

using Google.Apis.Auth.OAuth2;
using Google.Apis.Services;
using Newtonsoft.Json.Linq;
using System.IO;

namespace GoogleAPITest
{
    /// <summary>
    /// GoogleAPI利用においての規定クラス
    /// </summary>
    public abstract class GoogleAPIBase<T> where T : IClientService
    {
        /// <summary>
        /// クライアントサービスインターフェース
        /// </summary>
        protected T Serive { get; set; }

        /// <summary>
        /// コンストラクタ
        /// </summary>
        /// <param name="keyJsonPath">APIキーのJSONファイルのパス</param>
        public GoogleAPIBase(string keyJsonPath, string[] scope)
        {
            var jObject = JObject.Parse(File.ReadAllText(keyJsonPath));
            var serviceAccountEmail = jObject["client_email"].ToString();
            var privateKey = jObject["private_key"].ToString();

            var credential = new ServiceAccountCredential(
            new ServiceAccountCredential.Initializer(serviceAccountEmail)
            {
                Scopes = scope
            }.FromPrivateKey(privateKey));

            this.Serive = this.CreateService(credential);
        }

        /// <summary>
        /// サービス作成メソッド
        /// </summary>
        /// <param name="credential">認証情報</param>
        /// <returns>クライアントサービスインターフェース</returns>
        protected abstract T CreateService(ICredential credential);
    }
}

カレンダーAPI以外でも使用できるように、サービス作成メソッドを継承先に強制し、サービスの型をジェネリックにしている。

カレンダーAPIテストクラス

using Google.Apis.Auth.OAuth2;
using Google.Apis.Calendar.v3;
using Google.Apis.Calendar.v3.Data;
using Google.Apis.Services;
using System;

namespace GoogleAPITest.Calendar
{
    /// <summary>
    /// カレンダーAPIテストクラス
    /// </summary>
    public class CalendarAPITest : GoogleAPIBase<CalendarService>
    {
        /// <summary>
        /// アプリケーション名
        /// </summary>
        private const string APP_NAME = "Google Calendar API .NET";

        /// <summary>
        /// カレンダーテストクラス
        /// </summary>
        public CalendarAPITest(string keyJsonPath) : base(keyJsonPath, new string[] { CalendarService.Scope.Calendar })
        {
        }

        /// <summary>
        /// クライアントサービス作成
        /// </summary>
        /// <param name="credential">認証情報</param>
        /// <returns>クライアントサービスインターフェース</returns>
        protected override CalendarService CreateService(ICredential credential)
        {
            return new CalendarService(new BaseClientService.Initializer()
            {
                HttpClientInitializer = credential,
                ApplicationName = APP_NAME
            });
        }

        /// <summary>
        /// 予定読み取り
        /// </summary>
        /// <param name="calendarId">カレンダーID</param>
        public void ReadEvents(string calendarId)
        {
            // ここで第2引数にサービスアカウントに公開したカレンダーIDを指定する
            var request = new EventsResource.ListRequest(this.Serive, calendarId);
            request.TimeMin = DateTime.Now;
            request.ShowDeleted = false;
            request.SingleEvents = true;
            request.MaxResults = 10;
            request.OrderBy = EventsResource.ListRequest.OrderByEnum.StartTime;

            // List events.
            var events = request.Execute();
            Console.WriteLine("Upcoming events:");
            if (events.Items != null && events.Items.Count > 0)
            {
                foreach (var eventItem in events.Items)
                {
                    var when = eventItem.Start.DateTime.ToString();
                    if (String.IsNullOrEmpty(when))
                    {
                        when = eventItem.Start.Date;
                    }

                    Console.WriteLine("{0} start:({1}) end:({2})", 
                        eventItem.Summary, when, eventItem.End.DateTime.ToString());
                }
            }
            else
            {
                Console.WriteLine("No upcoming events found.");
            }
        }

        /// <summary>
        /// カレンダーイベントを追加
        /// </summary>
        /// <param name="calendarId">カレンダーID</param>
        public void InsertEvent(string calendarId)
        {
            var newEvent = new Event()
            {
                Summary = "Google I/O 2020",
                Location = "神奈川県横浜市",
                Description = "テスト備考",
                Start = new EventDateTime()
                {
                    DateTime = DateTime.Parse("2020/02/28 9:00:00"),
                    TimeZone = "Asia/Tokyo",
                },
                End = new EventDateTime()
                {
                    DateTime = DateTime.Parse("2020/02/28 17:00:00"),
                    TimeZone = "Asia/Tokyo",
                },
                //Recurrence = new string[] { "RRULE:FREQ=DAILY;COUNT=2" },
                //Attendees = new EventAttendee[] {
                //    new EventAttendee() { Email = "lpage@example.com" },
                //    new EventAttendee() { Email = "sbrin@example.com" },
                //},
                //Reminders = new Event.RemindersData()
                //{
                //    UseDefault = false,
                //    Overrides = new EventReminder[] {
                //        new EventReminder() { Method = "email", Minutes = 24 * 60 },
                //        new EventReminder() { Method = "sms", Minutes = 10 },
                //    }
                //}
            };

            var request = this.Serive.Events.Insert(newEvent, calendarId);
            var createdEvent = request.Execute();
            Console.WriteLine("Event created: {0}", createdEvent.HtmlLink);
        }
    }
}

プログラムメインエントリ

using Google.Apis.Auth.OAuth2;
using Google.Apis.Calendar.v3;
using Google.Apis.Services;
using Google.Apis.Util.Store;
using GoogleAPITest.Calendar;
using Newtonsoft.Json.Linq;
using System;
using System.IO;
using System.Threading;

namespace GoogleAPITest
{
    /// <summary>
    /// メインクラス
    /// </summary>
    public class Program
    {
        /// <summary>
        /// メインエントリ
        /// </summary>
        /// <param name="args">実行時引数</param>
        public static void Main(string[] args)
        {
            try
            {
                // カレンダーID
                var calendarId = "カレンダーID";

                // Googleカレンダーテストクラスインスタンス化
                var calApi = new CalendarAPITest(
                    @"C:\job\TestProject\GoogleAPITest\testproject-269217-813bf9be17a5.json");

                // イベント読み取り
                calApi.ReadEvents(calendarId);

                // イベント追加
                calApi.InsertEvent(calendarId);
            }
            catch (Exception err)
            {
                Console.WriteLine(err.Message);
            }
            finally
            {
                Console.Read();
            }
        }
    }
}

次回は更新をやってみたいと思います。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Google Calendar APIを使用してみる #2

シリーズ

Google Calendar APIを使用してみる #1
Google Calendar APIを使用してみる #3

環境

IDE:VisualStudio2019
アプリケーション:コンソールアプリ
フレームワーク:.NET Core 3.1

カレンダーに予定を追加

前回は予定取得を試してみました。
今回はカレンダーの予定追加/更新を行ってみます。

まずわかりやすいように自分のカレンダーの直近の予定を空にします。
image.png

予定を追加するコードを記述します。
以下公式にサンプルがありました。
Events: insert

以下のようなスケジュール追加メソッドを用意

        /// <summary>
        /// カレンダーイベントを追加
        /// </summary>
        /// <param name="calendarId">カレンダーID</param>
        public void InsertEvent(string calendarId)
        {
            var newEvent = new Event()
            {
                Summary = "Google I/O 2020",
                Location = "神奈川県横浜市",
                Description = "テスト備考",
                Start = new EventDateTime()
                {
                    DateTime = DateTime.Parse("2020/02/28 9:00:00"),
                    TimeZone = "Asia/Tokyo",
                },
                End = new EventDateTime()
                {
                    DateTime = DateTime.Parse("2020/02/28 17:00:00"),
                    TimeZone = "Asia/Tokyo",
                },

                //以下があるとエラーになっていたのでコメントアウト・・・
                //Recurrence = new string[] { "RRULE:FREQ=DAILY;COUNT=2" },
                //Attendees = new EventAttendee[] {
                //    new EventAttendee() { Email = "lpage@example.com" },
                //    new EventAttendee() { Email = "sbrin@example.com" },
                //},
                //Reminders = new Event.RemindersData()
                //{
                //    UseDefault = false,
                //    Overrides = new EventReminder[] {
                //        new EventReminder() { Method = "email", Minutes = 24 * 60 },
                //        new EventReminder() { Method = "sms", Minutes = 10 },
                //    }
                //}
            };

            var request = this.Serive.Events.Insert(newEvent, calendarId);
            var createdEvent = request.Execute();
            Console.WriteLine("Event created: {0}", createdEvent.HtmlLink);
        }

結果
image.png

サンプル全文

基底クラスを作りました。

using Google.Apis.Auth.OAuth2;
using Google.Apis.Services;
using Newtonsoft.Json.Linq;
using System.IO;

namespace GoogleAPITest
{
    /// <summary>
    /// GoogleAPI利用においての基底クラス
    /// </summary>
    public abstract class GoogleAPIBase<T> where T : IClientService
    {
        /// <summary>
        /// クライアントサービスインターフェース
        /// </summary>
        protected T Serive { get; set; }

        /// <summary>
        /// コンストラクタ
        /// </summary>
        /// <param name="keyJsonPath">APIキーのJSONファイルのパス</param>
        public GoogleAPIBase(string keyJsonPath, string[] scope)
        {
            var jObject = JObject.Parse(File.ReadAllText(keyJsonPath));
            var serviceAccountEmail = jObject["client_email"].ToString();
            var privateKey = jObject["private_key"].ToString();

            var credential = new ServiceAccountCredential(
            new ServiceAccountCredential.Initializer(serviceAccountEmail)
            {
                Scopes = scope
            }.FromPrivateKey(privateKey));

            this.Serive = this.CreateService(credential);
        }

        /// <summary>
        /// サービス作成メソッド
        /// </summary>
        /// <param name="credential">認証情報</param>
        /// <returns>クライアントサービスインターフェース</returns>
        protected abstract T CreateService(ICredential credential);
    }
}

カレンダーAPI以外でも使用できるように、サービス作成メソッドを継承先に強制し、サービスの型をジェネリックにしている。

カレンダーAPIテストクラス

using Google.Apis.Auth.OAuth2;
using Google.Apis.Calendar.v3;
using Google.Apis.Calendar.v3.Data;
using Google.Apis.Services;
using System;

namespace GoogleAPITest.Calendar
{
    /// <summary>
    /// カレンダーAPIテストクラス
    /// </summary>
    public class CalendarAPITest : GoogleAPIBase<CalendarService>
    {
        /// <summary>
        /// アプリケーション名
        /// </summary>
        private const string APP_NAME = "Google Calendar API .NET";

        /// <summary>
        /// カレンダーテストクラス
        /// </summary>
        public CalendarAPITest(string keyJsonPath) : base(keyJsonPath, new string[] { CalendarService.Scope.Calendar })
        {
        }

        /// <summary>
        /// クライアントサービス作成
        /// </summary>
        /// <param name="credential">認証情報</param>
        /// <returns>クライアントサービスインターフェース</returns>
        protected override CalendarService CreateService(ICredential credential)
        {
            return new CalendarService(new BaseClientService.Initializer()
            {
                HttpClientInitializer = credential,
                ApplicationName = APP_NAME
            });
        }

        /// <summary>
        /// 予定読み取り
        /// </summary>
        /// <param name="calendarId">カレンダーID</param>
        public void ReadEvents(string calendarId)
        {
            // ここで第2引数にサービスアカウントに公開したカレンダーIDを指定する
            var request = new EventsResource.ListRequest(this.Serive, calendarId);
            request.TimeMin = DateTime.Now;
            request.ShowDeleted = false;
            request.SingleEvents = true;
            request.MaxResults = 10;
            request.OrderBy = EventsResource.ListRequest.OrderByEnum.StartTime;

            // List events.
            var events = request.Execute();
            Console.WriteLine("Upcoming events:");
            if (events.Items != null && events.Items.Count > 0)
            {
                foreach (var eventItem in events.Items)
                {
                    var when = eventItem.Start.DateTime.ToString();
                    if (String.IsNullOrEmpty(when))
                    {
                        when = eventItem.Start.Date;
                    }

                    Console.WriteLine("{0} start:({1}) end:({2})", 
                        eventItem.Summary, when, eventItem.End.DateTime.ToString());
                }
            }
            else
            {
                Console.WriteLine("No upcoming events found.");
            }
        }

        /// <summary>
        /// カレンダーイベントを追加
        /// </summary>
        /// <param name="calendarId">カレンダーID</param>
        public void InsertEvent(string calendarId)
        {
            var newEvent = new Event()
            {
                Summary = "Google I/O 2020",
                Location = "神奈川県横浜市",
                Description = "テスト備考",
                Start = new EventDateTime()
                {
                    DateTime = DateTime.Parse("2020/02/28 9:00:00"),
                    TimeZone = "Asia/Tokyo",
                },
                End = new EventDateTime()
                {
                    DateTime = DateTime.Parse("2020/02/28 17:00:00"),
                    TimeZone = "Asia/Tokyo",
                },
                //Recurrence = new string[] { "RRULE:FREQ=DAILY;COUNT=2" },
                //Attendees = new EventAttendee[] {
                //    new EventAttendee() { Email = "lpage@example.com" },
                //    new EventAttendee() { Email = "sbrin@example.com" },
                //},
                //Reminders = new Event.RemindersData()
                //{
                //    UseDefault = false,
                //    Overrides = new EventReminder[] {
                //        new EventReminder() { Method = "email", Minutes = 24 * 60 },
                //        new EventReminder() { Method = "sms", Minutes = 10 },
                //    }
                //}
            };

            var request = this.Serive.Events.Insert(newEvent, calendarId);
            var createdEvent = request.Execute();
            Console.WriteLine("Event created: {0}", createdEvent.HtmlLink);
        }
    }
}

プログラムメインエントリ

using Google.Apis.Auth.OAuth2;
using Google.Apis.Calendar.v3;
using Google.Apis.Services;
using Google.Apis.Util.Store;
using GoogleAPITest.Calendar;
using Newtonsoft.Json.Linq;
using System;
using System.IO;
using System.Threading;

namespace GoogleAPITest
{
    /// <summary>
    /// メインクラス
    /// </summary>
    public class Program
    {
        /// <summary>
        /// メインエントリ
        /// </summary>
        /// <param name="args">実行時引数</param>
        public static void Main(string[] args)
        {
            try
            {
                // カレンダーID
                var calendarId = "カレンダーID";

                // Googleカレンダーテストクラスインスタンス化
                var calApi = new CalendarAPITest(
                    @"C:\job\TestProject\GoogleAPITest\testproject-269217-813bf9be17a5.json");

                // イベント読み取り
                calApi.ReadEvents(calendarId);

                // イベント追加
                calApi.InsertEvent(calendarId);
            }
            catch (Exception err)
            {
                Console.WriteLine(err.Message);
            }
            finally
            {
                Console.Read();
            }
        }
    }
}

次回は更新をやってみたいと思います。
次回

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む