【C#】ソースで動的に追加したコントロール - 3.DBから動的に追加＆参照

# コントロールをデータベース(DBのマスタデータ)の内容から、動的にフォームへ追加して使いたい

## C#の記事目次
- [C#でMySQLを使う - 1.開発環境インストール](https://qiita.com/kazumi75kitty/items/9953bb84edd7717bc90a)
- [C#でMySQLを使う - 2.SELECT・画面表示](https://qiita.com/kazumi75kitty/items/dd391cab06762555dc5d)
- [C#でMySQLを使う - 3.追加更新と削除](https://qiita.com/kazumi75kitty/items/65f8ae261bbe61964627)
- [C#でのデータベースのデータのNULLかどうかの判定](https://qiita.com/kazumi75kitty/items/64002467350a1ea08ab0)
- [C#のdelegate(デリゲート)と=>(ラムダ式)はC言語の関数へのポインタに似た扱い](https://qiita.com/kazumi75kitty/items/fdf4e1d2ea8e9c9326ba#comment-dbed4d53e43dbfdf5baa)
- [動的にコントロールを追加 - 1.ボタンクリック](https://qiita.com/kazumi75kitty/items/2e3f4c10a93d26032382)
- [動的にコントロールを追加 - 2.テキストボックスの参照](https://qiita.com/kazumi75kitty/items/093b1d679e016782d3af)
- 動的にコントロールを追加 - 3.DB(MySQL)から動的に作成【この記事】

### 前置き
C#の新規記事が2年ぶりになってしまいましたが…
というのも、

- ちょうど2年前は…コロナ禍以前と全く変わらない勤務とビジネスのスタイルに従事していた。大企業のオフィスに出社して、大企業のために働くという、コロナ禍を機に見直すべきスタイルとは異なっていたため、地元経済への恩恵には至らず、私のQiitaを使ったエンジニア貢献の意義が消えてしまった
- 去年はテレワークもあったものの、商用側などエッセンシャルワーキングに従事する時はもちろんセキュリティ上欠かさず出社するが、その案件はJavaに従事していた。その際、拠点が遠すぎるため、おまけに深夜まで残業することもあり、C#の着手を忘れていた
- C#はそもそも後回しにしていて、PHPの私立な三浦半島のWebアプリ[https://kazumi-jam.chips.jp/yokosuka/photo_001/](https://kazumi-jam.chips.jp/yokosuka/photo_001)のコンテンツ作成に集中する必要があったため
- その他、プログラミングよりもあつ森の島クリエイターなどに夢中になっていた【笑

という2年間のQiita放置となってしまいましたが、久方C#の記事も書く気にもなりましたので、引き続きC#の動的コントロール追加を追記してまいります。

[前回はソースで動的にフォームへ追加したテキストボックスを、this.Controls.Find()というメソッドを使ってNameで探すことによって、入力テキストを参照する](https://qiita.com/kazumi75kitty/items/093b1d679e016782d3af)手法を試していました。しかしながら、動的のコントロールの元は、ソースコードにハードコーディングされた配列から動的コントロールを作っている状態で、動的生成の意味はないものです。

敢えてそうしているのは、**いきなりDBを使うのではなく、文字列(string)の形から、動的なコントロールの追加や参照ができるかどうかを試すための基礎練習的なやり方をする**ことが目的で、いきなりMySQLで取得して…動的コントロールに変換して…と一度にやると、初心者にとっては頓挫してしまうことが多いので、まずはサッカーやスケボーなどの基礎練習をやるのと同じようにしていきたいと思います。

話は長くなりましたが、今回はいよいよ、**データベース(MySQL)から(フォーム部品のマスタ)データを取得し、そこから動的なコントロールを配置したり、参照したりする**段階に入ります。

# 概要
## データベース環境
### MySQLサーバー
テスト用ユーザー：*test* (パスワード：test1)
データベース：**manutest**

### 作成するテーブル

フォーム部品マスタデータ「**cmpnnt_mstr**」
以下のように定義する

||name|type|tgt_grp_no|init_val|
|:-:|:-:|:-:|:-:|:-:|
|型|VARCHAR(64)|INT|INT|VARCHAR(1024)|
|必須属性|NOT NULL|NOT NULL|NOT NULL|初期値NULL|
|説明|コントロールのName|コントロールの種別|グループ番号|表示テキスト|

typeの凡例…
0：ラベル / 1：ボタン / 2：テキスト / 3：チェックボックス

このフォーム部品マスタテーブルは、グループ番号(tgt_grp_no)を特定して、それに該当するコントロールを取得する仕組みである


## やりたいこと
![やりたいこと](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/617906/c7f1b23e-d653-bc12-8691-d0a427d08ef9.jpeg)
1. あらかじめ、グループ番号を指定(ここでは簡単のためソースコードに直接記述する)して、どのフォーム部品をまとめて取ってくるかを番号で指定する
2. C#でWindowsフォームプログラムを開始すると、水色の破線枠のMain()メソッドが実行される。そこで、データベース(MySQL)からのフォーム部品のマスタデータを取得する
3. Main()メソッドの最後にフォームの表示処理にたどり着く。
4. フォームへのマスタデータのボタンやテキストボックスといったコントロールは、DBからの取得時に共通(static)な配列フィールドに格納され、その共通フィールドにある配列から動的にコントロールが定義される(黄緑の元気そうな枠)


## 前提
[「C#でMySQLを使う - 1.開発環境インストール」](https://qiita.com/kazumi75kitty/items/9953bb84edd7717bc90a)のように、Visual StudioとMySQLをインストールし、かつMySQL Connectors .NETがインストールされていること(Visual Studioは自動でMySQL Connectorsのライブラリを認識してくれる)

### マスタテーブルの中身
フォーム部品マスタテーブル「cmpnnt_mstr」を、あらかじめ以下のようにデータを追加しておきます。

|name|type|tgt_grp_no|init_val|
|:-:|:-:|:-:|:-:|
|txbUraga|2|0|浦賀|
|lblKamakura|0|0|鎌倉|
|txbMisaki|1|0|三崎|
|chkYokosuka|3|1|横須賀中央|
|lblMikasa|0|1|三笠公園|
|chkKannonzaki|3|0|観音崎|
|txbMabori|2|1|馬堀海岸|

# データベースからの動的コントロールの元となるフォーム部品マスタ取得

## Main()関数のあるProgram.csでの実装

最初に、肝となるマスタデータのDB取得を実装してみましょう( ∩'-'⊂ )
というのも、コントロールを表示させることばかり集中してしまうと、パニックになってしまうので、今回はDBを使う大前提となるので、基幹になっているDB取得で、まずはMySQLからマスタデータを取得してから、C#の配列に入れる、という作業から始めましょう！

```c#:Program.cs
using System;
using System.Collections.Generic;
using System.Windows.Forms;

using MySql.Data.MySqlClient;   // これがMySQLを扱うためのオブジェクトを含んでいる

namespace MyTrainingCsFrm1
{
    // 取得マスタデータの型とフォーム部品の情報クラス
    public class MasterData
    {
        public string name { get; set; }
        public int type { get; set; }
        public string initVal { get; set; }
    }

    // プログラムのエントリーポイント
    static class Program
    {
        // ここでは簡単のため、設定などプログラム内共有変数はエントリーポイントのProgram.csに記述することとする

        // 使用するマスタデータのフォーム部品グループ番号
        public static int usePartsGrpNo { get; set; }

        // 取得マスタデータ
        public static List<MasterData> CmpnntMstr { get; set; }

        // マスタデータから取得したフォーム部品数
        public static int cmpnntNum { get; set; }
        

        /// <summary>
        /// アプリケーションのメイン エントリ ポイントです。
        /// </summary>
        [STAThread]
        static void Main()
        {
            // 最初の設定(フォーム部品グループ番号)
            Program.usePartsGrpNo = 0;

            // マスタデータ領域の初期化
            Program.CmpnntMstr = new List<MasterData>();

            // マスタデータの部品数リセット
            Program.cmpnntNum = 0;

            // 最初にマスタデータを読み込むため、メイン関数にSQL接続を行う
            try
            {
                // 接続情報を文字列で与える
                string connStr = "server=127.0.0.1;user id=test;password=test1;database=manutest";
                MySqlConnection conn = new MySqlConnection(connStr);

                // 接続を開く
                conn.Open();

                // SQLを実行する
                MySqlCommand mstrCmd = new MySqlCommand(@"SELECT name, type, init_val FROM cmpnnt_mstr WHERE tgt_grp_no = @tgtno", conn);
                mstrCmd.Parameters.AddWithValue("tgtno", Program.usePartsGrpNo);
                MySqlDataReader mstrData = mstrCmd.ExecuteReader();

                // マスタデータを取得し、プログラム内で共有できるようにする
                while (mstrData.Read())
                {
                    String cmpnntName = mstrData.GetString(0);
                    int cmpnntType = mstrData.GetInt32(1);
                    String initValue = mstrData.GetString(2);

                    MasterData md = new MasterData();
                    md.name = cmpnntName;
                    md.type = cmpnntType;
                    md.initVal = initValue;
                    Program.CmpnntMstr.Add(md); // マスタデータの情報の追加

                    Program.cmpnntNum++;        // 部品の数の加算
                }

                // 接続を閉じる
                conn.Close();
            }
            catch (MySqlException mse)
            {
                // MySqlの接続エラー処理をここに書く
                Console.WriteLine(mse.Message);
                MessageBox.Show("MySQLサービスが起動しているか、DB「manutest」がユーザー「test」で試験接続できるか確認してください", "MySQLエラー", MessageBoxButtons.OK, MessageBoxIcon.Error);
                return;
            }
            
            // 接続が成功したらフォームを開く
            Application.EnableVisualStyles();
            Application.SetCompatibleTextRenderingDefault(false);
            Application.Run(new Form1());
        }
    }
}

```

最初に、Program.csでMain()メソッドというエントリーポイントからWindowsフォームアプリケーションが実行されるので、エントリーポイントのクラスは「static class Program」に該当します。

```c#
    // 取得マスタデータの型とフォーム部品の情報クラス
    public class MasterData
    {
        public string name { get; set; }
        public int type { get; set; }
        public string initVal { get; set; }
    }
```

いきなり冒頭に新しいクラス定義が現れましたｗｗ
こちらはテーブル「cmpnnt_mstr」から取ってくるデータをC#の形として扱いやすいように、クラス定義したものです。

```c#
                // SQLを実行する
                MySqlCommand mstrCmd = new MySqlCommand(@"SELECT name, type, init_val FROM cmpnnt_mstr WHERE tgt_grp_no = @tgtno", conn);
                mstrCmd.Parameters.AddWithValue("tgtno", Program.usePartsGrpNo);
                MySqlDataReader mstrData = mstrCmd.ExecuteReader();
```

上の箇所が、フォーム部品のマスタデータ「cmpnnt_mstr」を、コントロールのNameと種別、表示テキストという、フォームに動的コントロール定義するのに必要なカラムをSELECTし、グループ番号は検索キーとしています。

ただ、今回は簡単のため、以下のようにフォーム部品のグループ番号はMain()メソッド内でハードコーディングしていますが、実運用では設定ファイルなどで柔軟に対応するべきでしょう(\*˘꒳˘\*)

```c#
            // 最初の設定(フォーム部品グループ番号)
            Program.usePartsGrpNo = 0;
```

DBからマスタデータを取得したら、ここでは「**CmpnntMstr**」という共通フィールドとして、**マスタデータの配列に取得したフォーム部品のマスタデータ**を入れています。このstaticな配列をpublicで定義することによって、フォーム本体からも取得したフォーム部品マスタデータを参照することができるようになります。

```c#
        // 取得マスタデータ
        public static List<MasterData> CmpnntMstr { get; set; }
```

このフィールドも今回はソースコードを行ったり来たりしてソースファイル数を増やしたくないので、Program.csのstaticフィールドに入れてしまっています(˶ ･ᴗ･ )੭⚐⚑

# フォーム側の実装
## 画面レイアウト
[前回](https://qiita.com/kazumi75kitty/items/093b1d679e016782d3af)を踏襲して、「フォームを呼び出す」ボタンで動的コントロール配置することにします(˶ ･ᴗ･ )੭.ᐟ.ᐟ

![フォームデザイン](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/617906/0f764ff4-7768-57bd-6490-7c34b7f2a399.png)

- 「フォームを呼び出す」ボタン「formCallButton」：「Program.cs」の共通フィールド「CmpnntMstr」という配列から、取得済みフォーム部品より動的にコントロールを定義する
- 「フォームの内容を確認する」ボタン「buttonCheckEntry」：呼び出したフォーム部品から生じた動的コントロールをすべて参照する

## フォームを呼び出す動的コントロール配置処理の実装
フォームを呼び出す動的コントロール配置処理「formCallButton_Click」

```c#:Form1.cs (formCallButton_Clickまでの部分)
using System;
using System.Drawing;
using System.Text;
using System.Windows.Forms;

namespace MyTrainingCsFrm1
{
    public partial class Form1 : Form
    {
        // フォーム呼び出し状態(true：呼び出し済み / false：未呼び出し)
        private bool called = false;


        public Form1()
        {
            InitializeComponent();
        }

        // フォームを呼び出すボタン
        private void formCallButton_Click(object sender, EventArgs e)
        {
            if (this.called == true)
            {
                MessageBox.Show("すでに表示されています");
                return;
            }

            // マスタデータからの部品を定義する
            for (int i = 0; i < Program.CmpnntMstr.Count; i++)
            {
                MasterData cmpnnt = Program.CmpnntMstr[i];  // マスタデータからの単一フォーム部品定義

                // 種類ごとに部品を定義する
                switch (cmpnnt.type)
                {
                    case 0:     // ラベル
                        // インスタンス作成
                        Label lbl = new Label();
                        lbl.Name = cmpnnt.name;
                        lbl.Text = cmpnnt.initVal;

                        // サイズと配置
                        lbl.Size = new Size(100, 20);
                        lbl.Location = new Point(10, 10 + i * 22);

                        // フォームへの追加
                        this.Controls.Add(lbl);
                        break;

                    case 1:     // ボタン
                        // インスタンス作成
                        Button btn = new Button();
                        btn.Name = cmpnnt.name;
                        btn.Text = cmpnnt.initVal;

                        // サイズと配置
                        btn.Size = new Size(100, 20);
                        btn.Location = new Point(10, 10 + i * 22);

                        // フォームへの追加
                        this.Controls.Add(btn);
                        break;

                    case 2:     // テキストボックス
                        // インスタンス作成
                        TextBox tbx = new TextBox();
                        tbx.Name = cmpnnt.name;
                        tbx.Text = cmpnnt.initVal;

                        // サイズと配置
                        tbx.Size = new Size(100, 20);
                        tbx.Location = new Point(10, 10 + i * 22);

                        // フォームへの追加
                        this.Controls.Add(tbx);
                        break;

                    case 3:     // チェックボックス
                        // インスタンス作成
                        CheckBox cbx = new CheckBox();
                        cbx.Name = cmpnnt.name;
                        cbx.Text = cmpnnt.initVal;

                        // サイズと配置
                        cbx.Size = new Size(100, 20);
                        cbx.Location = new Point(10, 10 + i * 22);

                        // フォームへの追加
                        this.Controls.Add(cbx);
                        break;

                }
            }

            // フォームが呼び出された状態
            this.called = true;

            // フォーム呼び出し後は、フォーム部品の内容確認ボタンを有効化する
            buttonCheckEntry.Enabled = true;
        }
    (以下表記中断)
```

以下の部分で、Program.csの取得したマスタデータ「CmpnntMstr」という配列から、動的なコントロールの定義を行っています(ここでは、ソースコードをわかりやすくするために、staticにて参照して配列添え字参照をしているCmpnntMstrの構文を毎回記述する面倒なことはせず、「MasterData cmpnnt = Program.CmpnntMstr[i]」というように1つの一時変数に置いています)。

```c#
            // マスタデータからの部品を定義する
            for (int i = 0; i < Program.CmpnntMstr.Count; i++)
            {
                MasterData cmpnnt = Program.CmpnntMstr[i];  // マスタデータからの単一フォーム部品定義

                // 種類ごとに部品を定義する
                switch (cmpnnt.type)
                {
                    case 0:     // ラベル
                  (以下省略)
```

その際、コントロールの種別をswitch ～ caseで分岐し、例えば0ならラベル、2ならテキストボックスを新規コントロールオブジェクトとして定義して、フォームに張り付けています。

```c#
                    case 2:     // テキストボックス
                        // インスタンス作成
                        TextBox tbx = new TextBox();
                        tbx.Name = cmpnnt.name;
                        tbx.Text = cmpnnt.initVal;

                        // サイズと配置
                        tbx.Size = new Size(100, 20);
                        tbx.Location = new Point(10, 10 + i * 22);

                        // フォームへの追加
                        this.Controls.Add(tbx);
                        break;
```

サイズはコントロールの張り付ける番号で順次縦に並べて配置する至ってシンプルなやり方です(「tbx.Location = new Point(10, 10 + i * 22)」、これは相対座標を(10, 10 + i * 22)というように、コントロールの参照番号のiを22倍ずつ縦座標の設定をしている)。ラベルやテキストボックスの内容もマスタテーブルのinit_valをTextプロパティに与えるだけです(ただでさえDBから動的コントロールってQiitaなどでさえ説明するのは一筋縄ではわかりにくいので、とにかく必要なことだけを順次記事にしていきたいので…)。

その定義の際、肝となってくるのが「*tbx.Name = cmpnnt.name*」というように、**マスタテーブルのnameが、動的なコントロールの識別名であること**です。それが、後の動的に追加されたフォームの参照に必要となってくるのです。

### フォームを呼び出した動作確認
ここでは「Program.cs」のグループ番号を0(「Program.usePartsGrpNo = 0」)にしてフォーム部品を取得するので「マスタテーブルの中身」のセクションの「cmpnnt_mstr」は、tgt_grp_noが0のものが表示されるだろうと思うので、表示してみます。

![グループ番号が0](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/617906/2b7a6a13-5154-4fef-01a9-da6988cfd78f.png)

Visual Studioでハードコーディングされていなくても、動的にコントロールが配置されているところが確認できます。


## フォームの内容を確認するボタンで、動的に配置されたコントロールを参照する処理の実装
では、DBで動的に配置されたコントロールが、実際に参照できるかどうかを試します(˶ ･ᴗ･ )੭⚐⚑

```c#:Form1.cs (残り後半の部分)
   (Form1.csの続き表記)
        // 動的に呼び出したフォーム部品の内容を確認するボタン
        private void buttonCheckEntry_Click(object sender, EventArgs e)
        {
            StringBuilder sb = new StringBuilder();

            // フォーム部品を識別名で探し、その内容を取得する
            for (int i = 0; i < Program.CmpnntMstr.Count; i++)
            {
                sb.Append("[" + i + "]");

                MasterData cmpnnt = Program.CmpnntMstr[i];  // マスタデータからの単一フォーム部品定義

                // フォーム上の識別名に該当するコントロールを探す
                Control[] cmpnntCtls = this.Controls.Find(cmpnnt.name, false);

                for (int j = 0; j < cmpnntCtls.Length; j++)
                {
                    // 取得したフォーム部品
                    switch (cmpnnt.type)
                    {
                        case 0:     // ラベル
                            // フォーム部品のNameと種類
                            sb.Append(cmpnnt.name);
                            sb.Append("(ラベル):");

                            // ラベルテキストを文字列取得
                            Label lbl = (Label)cmpnntCtls[j];
                            sb.Append(lbl.Text);

                            break;

                        case 1:
                            // フォーム部品のNameと種類
                            sb.Append(cmpnnt.name);
                            sb.Append("(ボタン):");

                            break;

                        case 2:     // テキストボックス
                            // フォーム部品のNameと種類
                            sb.Append(cmpnnt.name);
                            sb.Append("(テキストボックス):");

                            // 内容を文字列取得
                            TextBox tbx = (TextBox)cmpnntCtls[j];
                            sb.Append(tbx.Text);
                            break;

                        case 3:
                            // フォーム部品のNameと種類
                            sb.Append(cmpnnt.name);
                            sb.Append("(チェックボックス):");

                            // チェック状態を文字列取得
                            CheckBox cbx = (CheckBox)cmpnntCtls[j];
                            sb.Append(cbx.Checked);

                            break;

                    }

                }
                
                // 最終要素以外の場合は「/」で区切る
                if (i < Program.CmpnntMstr.Count - 1)
                {
                    sb.Append(" / ");
                }
            }

            // メッセージを表示する
            MessageBox.Show("表示中の要素は…" + sb.ToString() + " です。");
        }
    }
}
```

まずはマスタデータの取得したフォーム部品の数分、コントロールのNameでコントロールをFind()します。

```c#
            // フォーム部品を識別名で探し、その内容を取得する
            for (int i = 0; i < Program.CmpnntMstr.Count; i++)
            {
                sb.Append("[" + i + "]");

                MasterData cmpnnt = Program.CmpnntMstr[i];  // マスタデータからの単一フォーム部品定義

                // フォーム上の識別名に該当するコントロールを探す
                Control[] cmpnntCtls = this.Controls.Find(cmpnnt.name, false);
```

「*Control[] cmpnntCtls = this.Controls.Find(cmpnnt.name, false)*」で探した動的コントロールに対し、さらにマスタデータのコントロールの種別「cmpnnt.type」で、ラベルならラベルテキストを、テキストボックスなら入力テキストを、そしてチェックボックスならチェックの状態を拾って文字列として組み立てて、動的コントロールのフォーム入力の内容をメッセージボックスにて表示しています。

### 動的コントロールを参照した動作確認
![動的コントロールを参照](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/617906/a1f8cceb-1914-2e5a-adf9-070f52f0860b.png)

ここではグループ番号を0にてマスタデータを取得しているので、上の図の通りの入力だと、メッセージボックスは想定通りとなると思います(\*´˘`\*)♡

# DBの取得内容に応じて動的にコントロールが扱えるかの確認

ここではフォーム部品マスタテーブル「cmpnnt_mstr」をグループ番号を「Program.cs」であらかじめ設定して、該当するグループ番号のフォーム部品をまとめて動的コントロール配置するやり方ですので、実際に動的にコントロールがDBの取得内容に応じて変更したい場合は、取得対象のグループ番号を変えてみます(˶ ･ᴗ･ )੭

```diff_c:Program.cs (グループ番号指定の箇所)
            // 最初の設定(フォーム部品グループ番号)
+           Program.usePartsGrpNo = 1;
-           Program.usePartsGrpNo = 0;
```

そうすると、今度は「cmpnnt_mstr」の「tgt_grp_no」が0ではなく1のものがフォームに呼び出されるだろうと思いますが、確認します。

![グループ番号が1](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/617906/dfaa4872-4284-60ad-6765-9f267fc7f85d.png)

![動的コントロールを参照](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/617906/d51da647-4f0c-d042-8c84-af49fafecbda.png)

動的コントロールの呼び出し、参照ともにうまくいっています( ˶˙ᵕ˙˶ )♥

# おわりに

記事が難しく、大変長くなって申し訳ありませんが、MySQLなどの**DBから動的にコントロールを定義＆参照することは、コントロールのインスタンスにNameを与えて、Find()メソッドでそのNameを与えることで可能**という結論となりました(\*˘꒳˘\*)

2年前の現場の案件で、1つのASP.NET(.aspx)ページで動的にテキストフォームなどのコントロールを配置する際、DBで動的に対応できないか、とのオーダーでしたので、それをきっかけに、一人も多くこのセンスをQiitaにて共有するに至りました。

# 参照文献

- [【.NET】コントロールを実行時に作成する](https://dobon.net/vb/dotnet/control/addcontrol.html)
- [【.NET】イベントを動的に登録する](http://www.r-nakai.com/archives/19)
- [コントロール名を表す文字列からコントロールを取得する - 文字列名のstring型からのコントロールのアクセス - C#プログラミング](https://www.ipentec.com/document/csharp-get-control-by-name-of-control-string)
- [C#_プログラムから生成した ( 動的 ) コントロールにイベントをつける](http://koshinran.hateblo.jp/entry/2017/11/17/193251)
- [Control.ControlCollection.Find(String, Boolean) メソッド](https://docs.microsoft.com/ja-jp/dotnet/api/system.windows.forms.control.controlcollection.find?view=netcore-3.1) - Microsoft .NET 公式
- [Windowsフォーム上のコントロールを検索するには？［2.0以降、C#、VB］](https://www.atmarkit.co.jp/fdotnet/dotnettips/479ctrlfind/ctrlfind.html) - atmark IT