週末副業記

土日は副業エンジニアのブログです。副業に関することを投稿します。

popIn Aladdin 2をSiriで音声操作する方法

f:id:aisakakun:20211003213809p:plain

こんにちは。相坂です。

popIn Aladdin 2 をSiriから音声操作したいという方のご参考になればと思います。

popIn Aladdin 2 はリモコンの一部機能が赤外線のため、音声操作が可能になります。

最初に申し上げますが、popIn Aladdin2の機能のうち、私が確認する限り(2021.10.02時点)赤外線による操作が可能なのは下記機能のみです。

popIn Aladdinの他の機能については今のところリモレスを使用するしかないです。

私はリモレスも持っていますが、Siriを普段から使用することが多いため、Siriによる音声操作を実現したいと考えました。

今回音声操作を実現するにあたって購入する製品はSwitch bot(¥3,980)のみです。

設定方法

Switch botの取り扱い説明書に記載されている内容(アカウントの登録など)については説明を省きます。

Switch botWi-Fi:5.0GHz帯(末尾aなど)では使用できません。

popIn Aladdin 2 のリモコンの登録

登録順序

最初に登録までの一連の操作動画を掲載します。 youtu.be

  1. 「ホーム」タブ右上の「+」ボタンを押します。
  2. 「デバイスの追加」画面で「赤外線リモコン」を選択し、「その他」を選択します。
  3. 「ハブを選択」画面で「(任意名称)」を選択します。
  4. 「リモコンを保存」画面で「デバイス名」「ルーム」を入力後、「保存」を選択します。
  5. 以下のような画面が表示されるので「今すぐ使う」を選択します。
  6. 4.で入力した「デバイス名」が画面上部に表示され、画面中央部に「+」が表示されます。「+」ボタンを選択します。
  7. 他の赤外線機器と同様に、popIn Aladdin 2 のリモコンのボタン(プロジェクター電源ボタン等)を押すとボタンが登録できます。冒頭で述べた通り、以下のボタンが登録できることを確認しております。(2021.10.02時点)

全て登録できると、以下のような画面ができます。

f:id:aisakakun:20211003202005j:plain:w300

音声操作するための設定

最初に設定完了までの一連の操作の動画を掲載します。 youtu.be

以下は、上記登録順序においてデバイス名を「popIn Aladdin」とし、「プロジェクター電源」を操作する場合の設定です。デバイス名はどんなものでも良いです。

  1. 「シーン」タブ右上の「+」ボタンを選択します。
  2. 「シーン設定」画面に移るので、以下のように設定します。

全ての条件が満たされた時:「手動実行」

アクションを選択:(手順は下記の通り)

  • 「popIn Aladdin(デバイス名)」を選択
  • 「プロジェクター電源」ボタンを選択
  1. クラウドサービス」を選択
  2. 「保存」を選択
  3. 「Siri Shortcuts」を選択→「実行」を選択
  4. 音声コマンド:「(Hey Siriの後に続いて言う言葉)」を入力
  5. 「Siriに追加」を選択

動画の例ではHey Siriに続いて、「テスト」というとpopIn Aladdinのプロジェクター電源が操作できます。

まとめ

popIn Aladdin 2を音声操作するためのSwitch botへのボタン登録方法について紹介しました。

音声操作によりpopIn Aladdin 2 を操作しましたが、とりあえず電源をつけるということはできるので快適です。

エアコンや扇風機、照明など赤外線で制御できる製品を同時に制御(家を出るときは一括OFF,帰ったときは一括ONなど)できることはかなり嬉しいです。ぜひ試してみてください。

iOS アプリリリースに関する参考サイト一覧

f:id:aisakakun:20210926135956j:plain
Jan VašekによるPixabayからの画像
Swift UIでアプリリリースできました。アプリ作成にも時間がかかりますが、アプリ作成後のリリース準備も割と調べることが多かったのでまとめておきます。

アイコン作成について

  • Cacoo:アプリデザインの作成
  • App Icon Maker:アイコンセット(さまざまなサイズのアイコン)の作成 cacoo.com

各端末のスクリーンショット作成について

アプリリリース時に要求されるスクリーンショットのデザインについて記述されています。 thwork.net

プライバシーポリシーについて

アプリ申請時にプライバシーポリシーのページURLを掲載しなければなりません。 私の場合は、TODOリストのようなアプリなので簡単ですが下記のように記述しました。 infoaisaka.hatenablog.com

プライバシーポリシーのページのみ記事本文中の広告を非表示にする

プライバシーポリシーの記事本文中に広告を貼るのはどうなのか、と思いましたので非表示にしました。 www.yutorism.jp

バージョンアップの方法について

スクリーンショットを修正したい場合でもバージョンアップになると思います。 +ボタンを押すと、初回リリース時と同様に入力できる画面が表示されます。 i-app-tec.com

その他

アプリに広告を掲載する(Swift UI)

reasonable-code.com

プライバシーポリシー(アプリ名:食材リスト)

f:id:aisakakun:20210923220939p:plain

以下のとおり個人情報保護方針を定め、個人情報保護の仕組みを構築し、本サービスの利用者(以下、「利用者」といいます)の個人情報の保護を推進いたします。

取得する利用者情報と目的

利用者から提供を受けた情報

利用者からお問い合わせがあった場合、利用者の氏名、連絡先(メールアドレス、電話番号等)をご提供いただくことがあります。

取得方法

お問い合わせ時Twitter(https://twitter.com/aisaka_fukugyo)のDMのやり取りを通じて、利用者から氏名、連絡先を直接伺うことにより取得を行います。自動で取得する手段はありません。

通知・公表または同意取得の方法・利用者関与の方法

アプリから直接利用者情報を取得することはありません。

外部送信・第三者提供・情報収集モジュール

メールやTwitterのDMを通じて利用者より入力された情報(テキスト、数字、添付した画像)などをサーバーや外部サービスに送信・保存することは一切いたしません。利用者から入力された情報はお問い合わせに伴うやり取り終了後、全て削除いたします。

お問い合わせ

Twitter(https://twitter.com/aisaka_fukugyo)のDM

共通のプライバシーポリシーとの関係

共通のプライバシーポリシーを作成しておりません。 作成時、本プライバシーポリシーを改定することがあります。

免責事項

利用上の不具合・不都合に対して可能な限りサポートを行っておりますが、利用者が本アプリを利用して生じた損害に関して、開発元は責任を負わないものとします。

改定日・制定日

制定日:2021年9月23日

なんで私が副業を? 副業と起業で悩んで「副業」に決めた3つの理由

f:id:aisakakun:20210828161442p:plain こんにちは。相坂です。土日にプログラミング関係の副業をしている現役SEです。

独立精神のある方は、一度は独立起業を志したことがあると思います。

私も学生時代に起業を志した人間の一人ですが、漠然と起業を考えているだけでした。

そのため、独立起業は危険ではないか?や、副業で十分できることではないか?など考えが錯綜していました。

今回の記事では、最終的に私が副業を開始するに至った経緯について説明します。 副業として起業することも可能なので、一般的な意味とは異なりますが本記事ではそれぞれを以下のように定義して話を進めます。

副業:自分以外の人が代表を務める会社に所属している状態で、土日などに会社と関係のない業務を行うこと。

独立起業:会社を興し、会社の代表として業務を行うこと。

会社勤めだが、起業しようか迷っている

起業したいが、リスクは負いたくない

といった方のご参考になればと思います。

副業と起業で悩んで「副業」に決めた3つの理由

リスクを負いたくない

f:id:aisakakun:20210828144021j:plain Gino CrescoliによるPixabayからの画像

私が会社員となった理由の一つは「安定した収入が得られること」でした。

独立起業を行う場合、現在の安定した収入がそのままなくなることになります。

副業の場合は土日に仕事を行うだけで、安定した収入を維持したまま+αで収入が得られます。

起業アイデアがない

f:id:aisakakun:20210828153631j:plain chenspecによるPixabayからの画像

起業を行う場合、これ一本で食べていける、あるいはこれをどうしても実現したい、というアイデアが必要です。

私は自分の腕一本で売上を上げるような経験をしていないことに加え、どうしても実現したいこともありませんでした。

ネット興亡記というネット関連で戦い抜いてきた人のエピソードが書かれている本があります。その中で堀江貴文さんのエピソードもその一つとして記載されていますが、会社を興した当初はホームページの制作など受託業が主だったと記載されています。

このことから分かることは、安定した収益基盤なくしては実現したいことも実現できないということです。

従って、思いだけではやりたいことも叶わないため、「これ一本で食べていける」武器を身につけることが最優先であると考えました。

武器を身につけるための業務は、起業ではなく、土日に行う副業で十分満たすことができると考えました。

副業の延長線上に起業がある

f:id:aisakakun:20210828154840j:plain Gerd AltmannによるPixabayからの画像

副業で自分の武器を身につけることは、起業を行う際にも収益基盤として機能します。

また、受託での業務などお客様の課題解決に関する依頼を受けていくと、「市場にはこのようなことで困っている人がいるのか」など、市場のニーズを知ることができます。

副業で受託業務を進めていく中で市場のニーズをつかみ、技術向上も可能なため、副業を続けて売上を向上させていく延長線上に起業があると考えました。

まとめ

副業と起業で悩んで「副業」に決めた3つの理由を紹介しました。

副業はこれから盛んになると思いますので、早期に始めることで将来開始する人よりも一歩リードすることができます。

副業は独立起業に比べてコストリスクが低いので、起業を志す方も一度は副業をしてみてはいかがでしょうか。

お勧め動画 副業や起業を開始する場合は税についての勉強も必須です。下記チャンネルの動画は非常に参考になりますので、副業を行う人は視聴することを推奨いたします。


www.youtube.com

クラウドソーシング「ランサーズ」

クラウドソーシング「ランサーズ」

は、私が副業を始めるにあたり登録したサイト2つのうちの1つです。

積極的に提案することで受注に繋げることができます。過去の案件も残っておりますので、実力把握にも最適です。

副業で月10万円を達成するためのポイントはこちらから infoaisaka.hatenablog.com

副業エンジニアご検討の方必見! 土日業務のみで月10万円達成するためのポイント(2)

こんにちは。相坂です。

世間的にも副業はトレンドになっています。今後ますますその勢いは増していき、個人間の競争は激化していくと思います。

私もサラリーマンの傍ら、4月に副業を開始した人間の1人です。本記事では、私が副業開始から1ヶ月目で2万円3ヶ月目で月10万円の案件を受注するに至るまでにしてきたことをご紹介します。

  • プログラミングによる副業収入を得たい
  • プログラミングの実務経験がない
  • どのようにして案件を獲得すれば良いか悩んでいる といった方のご参考になればと思います。
  • 導入:技術的バックグラウンド
  • 戦略:戦う「場」を選ぶ についての前回記事はこちらから。 infoaisaka.hatenablog.com
  • 3ヶ月目で月10万円の案件を受注するまでにしてきたこと(2)
    • 各種情報調査
    • プロフィール設計を行う:兵站
      • プロフィールはコピーして置き換える
      • プロフィールの選定
    • 提案テンプレートを作成する:兵站
    • ポートフォリオを作成する
    • 1日1件提案する:戦術
  • まとめ
続きを読む

副業エンジニアご検討の方必見! 土日業務のみで月10万円達成するためのポイント(1)

こんにちは。相坂です。

世間的にも副業はトレンドになっています。今後ますますその勢いは増していき、個人間の競争は激化していくと思います。

私もサラリーマンの傍ら、4月に副業を開始した人間の1人です。本記事では、私が副業開始から1ヶ月目で2万円3ヶ月目で月10万円の案件を受注するに至るまでにしてきたことをご紹介します。

  • プログラミングによる副業収入を得たい
  • プログラミングの実務経験がない
  • どのようにして案件を獲得すれば良いか悩んでいる といった方のご参考になればと思います。

3ヶ月目で月10万円の案件を受注するまでにしてきたこと(1)

導入:技術的バックグラウンド

f:id:aisakakun:20210818084051j:plain こういった記事を見るときに私が気にしていたのは、「どのような技術的バックグラウンドがある人なのか?」ということです。プログラミングの実務経験がある方以外は、副業で仕事を受注することが難しいと考えていたためです。下記が私の簡単な技術的バックグラウンドです。

  • プログラミングスキル ・Python使用経験あり。大学院でシミュレーション用にプログラムを作成。 ・実務にてプログラムの納品経験なし。 ・趣味として、プログラミングを実施していた。  :過去ブログ記事 infoaisaka.hatenablog.com

  • 要件定義スキル ・実務にて4年間SE業務に従事。 ・お客様からの要求を仕様に落とし込むスキルあり。

要件定義スキルに関して、SEの方の多くは私と同じようなスキルレベルになるのではと考えております。プログラミングに関しては、過去ブログ記事を確認いただき、「これだったら自分もできる」と思ったのであればプログラミングスキルに関しても同じようなスキルレベルになるのではと考えております。

戦う「場」を選ぶ:戦略

f:id:aisakakun:20210816132501j:plain 私が最初に取り組んだことは、「どの分野で稼ぐか」を決めることです。 そもそも副業を始めた目的は「自分のプログラミングスキルでどのレベルまで稼げるかを知ること」でした。そのため、実施内容がプログラミングに関わることであることは外せない条件でした。プログラミングに関わらず、やりたいことが軸としてあり、その延長に副業があるという方は、「プログラミング」の箇所を「転売」など他のキーワードに当てはめて考えてみてください。

プログラミングに関わる分野で自分が稼げる「場」はどこであるかを考えました。最初に思いついたのは、クラウドワークス」「ランサーズ」などのクラウドソーシングでした。ここを「場」として設定しました。この段階では、他の「場」については調査しません。

クラウドソーシングサイトへの登録

クラウドワークス

クラウドソーシング「ランサーズ」

クラウドワークスとランサーズへ会員登録を行いました。会員登録をしなくとも、案件は確認することができます。登録する前にあなたのターゲットとなるような案件があるか確認してください。

まとめ

技術的バックグラウンド、戦略について紹介いたしました。案件に取り組むためには、「この案件で必要な機能は、〜と、〜だから〜の情報が必要でその手段の実現方法は、、」というところでSEとしての要求分析スキルが大いに役立ったように思います。SE経験がなくとも要求を機能に分解することができる方は案件を受注可能と考えます。 次の記事ではさらに細かい戦略・戦術を紹介いたします。 次の記事: infoaisaka.hatenablog.com

クラウドソーシング「ランサーズ」

クラウドソーシング「ランサーズ」

は、私が副業を始めるにあたり登録したサイト2つのうちの1つです。積極的に提案することで受注に繋げることができます。過去の案件も残っておりますので、実力把握にも最適です。

【WPF】進捗インジケータのサンプル【C#】【XAML】

f:id:aisakakun:20200802001009p:plain
進捗状況・プログレスバーなどで検索するも、思ったように出てこなかったので作成しました。※結論、最適な検索キーワードは「進捗インジケータ」「プログレストラッカー」のようです。進捗状況をプログレスバーで表すには適していない場合があります。例えば単純に何秒間かユーザーに待たせるような場合は、進捗しているわけではないためプログレスバー以外の手段を用いた方がよいと思います。

機能概要

ボタンをクリックすると、正方形を正方形上に配置することにより構成された進捗インジケータが動作を行います。

完成イメージ

f:id:aisakakun:20200802002040p:plain
完成イメージ

画面更新部

進捗状況をあらわすようにプログラムをC#で書いた場合でも画面を更新しなければ、それがGUIに反映されることはありません。そのため、必ず下記を加える必要があります。

private void DoEvents()
{
      DispatcherFrame frame = new DispatcherFrame();
      var callback = new DispatcherOperationCallback(ExitFrames);
      Dispatcher.CurrentDispatcher.BeginInvoke(DispatcherPriority.Background, callback, frame);
      Dispatcher.PushFrame(frame);
}

private object ExitFrames(object obj)
{
     ((DispatcherFrame)obj).Continue = false;
     return null;
} 

進捗インジケータ更新部

12個の正方形で構成されています。

private void ProgressAnimation(int time)//timeの範囲:0<=time<=11
{
      int arrayNum = time;
      int arrayNum1 = time - 1 >= 0 ? time - 1 : time - 1 + 12;
      int arrayNum2 = time - 2 >= 0 ? time - 2 : time - 2 + 12;
      rectProgressArray[arrayNum].Fill = progressColor0;
      rectProgressArray[arrayNum1].Fill = progressColor1;
      rectProgressArray[arrayNum2].Fill = progressColor2;
      for (int i = 0; i < 12; i++)
      {
           if (arrayNum != i && arrayNum1 != i && arrayNum2 != i)
           {
                rectProgressArray[i].Fill = progressColor3;
            }
       }
}

コード全文

ベタうちで書いていますので、リファクタリングして活用ください。

C#のコード部(MainWindow.xaml.cs)

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Data;
using System.Windows.Documents;
using System.Windows.Input;
using System.Windows.Media;
using System.Windows.Media.Imaging;
using System.Windows.Navigation;
using System.Windows.Shapes;
using System.Windows.Threading;

namespace WpfApp5
{
    /// <summary>
    /// MainWindow.xaml の相互作用ロジック
    /// </summary>
    public partial class MainWindow : Window
    {

        private static Rectangle[] rectProgressArray = new Rectangle[12];
        private static SolidColorBrush progressColor0 = new SolidColorBrush();
        private static SolidColorBrush progressColor1 = new SolidColorBrush();
        private static SolidColorBrush progressColor2 = new SolidColorBrush();
        private static SolidColorBrush progressColor3 = new SolidColorBrush();

        public MainWindow()
        {
            InitializeComponent();
            InitialRect();
        }

        private void InitialRect()
        {
            rectProgressArray[0] = rectProgress0;
            rectProgressArray[1] = rectProgress1;
            rectProgressArray[2] = rectProgress2;
            rectProgressArray[3] = rectProgress3;
            rectProgressArray[4] = rectProgress4;
            rectProgressArray[5] = rectProgress5;
            rectProgressArray[6] = rectProgress6;
            rectProgressArray[7] = rectProgress7;
            rectProgressArray[8] = rectProgress8;
            rectProgressArray[9] = rectProgress9;
            rectProgressArray[10] = rectProgress10;
            rectProgressArray[11] = rectProgress11;

            progressColor0.Color = Color.FromRgb(0, 128, 255);
            progressColor1.Color = Color.FromRgb(155, 205, 255);
            progressColor2.Color = Color.FromRgb(244, 249, 255);
            progressColor3.Color = Color.FromRgb(255, 255, 255);
        }
        //画面更新部
        private void DoEvents()
        {
            DispatcherFrame frame = new DispatcherFrame();
            var callback = new DispatcherOperationCallback(ExitFrames);
            Dispatcher.CurrentDispatcher.BeginInvoke(DispatcherPriority.Background, callback, frame);
            Dispatcher.PushFrame(frame);
        }

        private object ExitFrames(object obj)
        {
            ((DispatcherFrame)obj).Continue = false;
            return null;
        } 
  //画面更新部

        //進捗インジケータ更新部
        private void ProgressAnimation(int time)//timeの範囲:0<=time<=11
        {
            int arrayNum = time;
            int arrayNum1 = time - 1 >= 0 ? time - 1 : time - 1 + 12;
            int arrayNum2 = time - 2 >= 0 ? time - 2 : time - 2 + 12;
            rectProgressArray[arrayNum].Fill = progressColor0;
            rectProgressArray[arrayNum1].Fill = progressColor1;
            rectProgressArray[arrayNum2].Fill = progressColor2;
            for (int i = 0; i < 12; i++)
            {
                if (arrayNum != i && arrayNum1 != i && arrayNum2 != i)
                {
                    rectProgressArray[i].Fill = progressColor3;
                }
            }
        }
       //進捗インジケータ更新部

        private void Button_Click(object sender, RoutedEventArgs e)
        {
            int count = 0;

            while (true)
            {
                count++;
                Thread.Sleep(100);//早すぎるのでスリープ
                if (count == 12)
                {
                    count = 0;
                }
                ProgressAnimation(count);
                DoEvents();//画面を更新する
            }
        }
    }
}

xamlのコード部(MainWindow.xaml)

~部を下記に書き換えてご使用ください。

    <Grid Margin="0,0,528.333,366.667">
        <Canvas Grid.Row="0" Grid.ColumnSpan="2" Margin="0,0,40,0">
            <Rectangle Name ="rectProgress0"  Fill="White" HorizontalAlignment="Left" Height="6" Stroke="#FFA6A7AB" VerticalAlignment="Top" Width="6" StrokeThickness="0.5" Canvas.Left="124" Canvas.Top="16"/>
            <Rectangle Name ="rectProgress1" Fill="White" HorizontalAlignment="Left" Height="6" Stroke="#FFA6A7AB" VerticalAlignment="Top" Width="6"  StrokeThickness="0.5" Canvas.Left="132" Canvas.Top="16"/>
            <Rectangle Name ="rectProgress2" Fill="White" HorizontalAlignment="Left" Height="6" Stroke="#FFA6A7AB" VerticalAlignment="Top" Width="6"  StrokeThickness="0.5" Canvas.Left="140" Canvas.Top="16"/>
            <Rectangle Name ="rectProgress3" Fill="White" HorizontalAlignment="Left" Height="6" Stroke="#FFA6A7AB" VerticalAlignment="Top" Width="6" StrokeThickness="0.5" Canvas.Left="148" Canvas.Top="16"/>
            <Rectangle Name ="rectProgress11" Fill="White" HorizontalAlignment="Left" Height="6" Stroke="#FFA6A7AB" VerticalAlignment="Top" Width="6"  StrokeThickness="0.5" Canvas.Left="124" Canvas.Top="24"/>
            <Rectangle Name ="rectProgress10" Fill="White" HorizontalAlignment="Left" Height="6" Stroke="#FFA6A7AB" VerticalAlignment="Top" Width="6" StrokeThickness="0.5" Canvas.Left="124" Canvas.Top="32"/>
            <Rectangle Name ="rectProgress9" Fill="White" HorizontalAlignment="Left" Height="6" Stroke="#FFA6A7AB" VerticalAlignment="Top" Width="6"  StrokeThickness="0.5" Canvas.Left="124" Canvas.Top="40"/>
            <Rectangle Name ="rectProgress8" Fill="White" HorizontalAlignment="Left" Height="6" Stroke="#FFA6A7AB" VerticalAlignment="Top" Width="6"  StrokeThickness="0.5" Canvas.Left="132" Canvas.Top="40"/>
            <Rectangle Name ="rectProgress7" Fill="White" HorizontalAlignment="Left" Height="6" Stroke="#FFA6A7AB" VerticalAlignment="Top" Width="6"  StrokeThickness="0.5" Canvas.Left="140" Canvas.Top="40"/>
            <Rectangle Name ="rectProgress6" Fill="White" HorizontalAlignment="Left" Height="6" Stroke="#FFA6A7AB" VerticalAlignment="Top" Width="6"  StrokeThickness="0.5" Canvas.Left="148" Canvas.Top="40"/>
            <Rectangle Name ="rectProgress5" Fill="White" HorizontalAlignment="Left" Height="6" Stroke="#FFA6A7AB" VerticalAlignment="Top" Width="6"  StrokeThickness="0.5" Canvas.Left="148" Canvas.Top="32"/>
            <Rectangle  Name ="rectProgress4" Fill="White" HorizontalAlignment="Left" Height="6" Stroke="#FFA6A7AB" VerticalAlignment="Top" Width="6"  StrokeThickness="0.5" Canvas.Left="148" Canvas.Top="24"/>
        </Canvas>
        <Button Content="Button" HorizontalAlignment="Left" Height="22" Margin="33,17,-106,-39" VerticalAlignment="Top" Width="73" Click="Button_Click" Background="#FF6E71D1" Foreground="White"/>
    </Grid>

競技プログラミング メモ【辞書関連】【C#】

競技プログラミングで使用する辞書方の文法を記述します。

辞書型

Dictionary<string,string> dic = new Dictionary<string,string>();
//追加
dic.Add("01","sample");
//値の取得
string name;
bool search = dic.TryGetValue("01",out name);

辞書の中に指定のキーが含まれるか

string S = Console.ReadLine();
var hs = new Dictionary<char,int>();
hs.ContainsKey(S[0])); //含まれたらtrue,含まれなかったらfalse

辞書よりもListの方がスタックオーバーフローにならない(経験談

Dictionary<int,int> A = new Dictionary<int,int>();
//上記と同じ物をListで書く場合、
List<Tuple<int,int>> A = new List<Tuple<int,int>>();
//同じ内容ならList<Tuple<int,int>>だとスタックオーバーフローにならない場合がある

辞書の文字列型Keyをソートする(ABC155C問題)

List型にしてString型のキーをソートする。(a~z順)

//ここまででdictにDictionary<string,int>でキーと値をセットします。
var ans = new List<string>();
foreach(var s in dict){
   ans.Add(s.Key);
}
ans.Sort(StringComparer.Ordinal);