Ліричний вступ

Якось, одного звичайнісінького буденного вечора, сидячи біля вогнища в печері :) я зрозумів, чого в житті мені не вистачало більше всього… Мені все життя не вистачало годинника говоруна. Я зрозумів справжню необхідність даної програми і почав її розробку. Даним процесом я вирішив поділитись з читачем. Дехто з читачів можливо здивується, чому я пишу українською (адже основна мова ресурсу — російська), та дивуватися тут нічого, адже це моя рідна мова і мені закортіло написати саме нею. В даному викладені ми коротко розглянемо етапи розробки від початку і до кінця, від формулювання задачі і до її імплементації.

 

Мета статті

Чим може бути корисна дана стаття? Є кілька варіантів:

  • можна побачити як розробляється реальна програма;
  • можна засвоїти кілька корисних прийомів програмування;
  • може виникнути бажання встановити програму собі на ПК і почати користуватись нею;
  • стаття написана нецікаво і читач просто змарнував свій час читаючи її :) .

 

Технічне завдання

Для початку необхідно формалізувати технічне завдання (ТЗ). Отже, технічне завдання:

«Це повинна бути програма, що із заданою періодичністю або ж у заданий час повинна озвучувати час. До додаткових можливостей відноситься запис власної версії озвучування користувачем а також вибір часу озвучування (мати можливість вказати, коли саме озвучувати час). Графічний інтерфейс — довільний.»

Тепер маємо завдання і можемо переходимо до процесу розробки.

 

Розробка програми

Інтерфейс користувача. Тут головне не перестаратись. Графічний інтерфейс має бути мінімально простим і максимально зручним в той же час. Це робиться для того, щоб користувач якомога швидше почав користуватись основним функціоналом програми. Було вирішено не робити головного вікна програми, а обійтись всього лиш піктограмою в панелі задач. Одразу після запуску програми піктограма з’являється в панелі задач і подальша робота з програмою зводиться до можливості правою кнопкою миші здійснювати додаткові операції  (наприклад, налаштування, отримання допомоги по роботі з програмою тощо).

Після натискання піктограми лівою кнопкою миші з’являється меню. В даному меню прописані команди керування програмою. Перелік обов’язкових команд наступний:

  • «Налаштування» для здійснення налаштування програми. Це і встановлення коли програма повинна озвучувати час і підпрограма запису звукових фрагментів безпосередньо для самого процесу озвучування часу;
  • «Допомога» для асистування користувача, щоб показати як правильно користуватись програмою.
  • «Про програму». В цьому пункті має бути вказана версія програми а також представлені відомості про розробника.
  • «Вийти» для того, щоб закрити програму;

Ресурси, необхідні для програми це:

  • Піктограма для програми (можна знайти тут);
  • Набір звукових файлів (все, що потрібно для того, щоб озвучити час). Так як озвучення часу щохвилини вимагає значної кількості файлів я вирішив скоротити їх кількість шляхом збільшення можливого інтервалу озвучення. В результаті було вирішено забезпечити користувача можливістю озвучування кожні 10 хвилин.

З необхідними ресурсами наче розібрались.

Реалізацію програми можна поділити на кілька етапів:

  • створюємо основну форму «Обробник» (FormMain) (хоч її і не повинно бути в фінальному варіанті програми). На останній стадії розробки ми її приховаємо;
  • створюємо форму «Налаштування» (FormSettings);
  • створюємо форму для озвучування програми «Персоналізація» (FormPersonalization);
  • створюємо форму «Про програму» (FormAbout);

Для читача основними і найцікавішими об’єктами є основна форма і форма озвучування (блок «Персоналізація«). Спочатку розглянемо блок «Обробник«, адже він являє собою основного обробника — мозок програми.

Блок «Обробник». Алгоритм основної частини роботи програми наступний:

  • Запуск програми;
  • Ініціалізація і запуск таймера. Період для основного таймера — 1 секунда;
  • По таймеру перевіряємо, чи настав момент, коли потрібно озвучити час. Якщо настав — зупиняємо таймер, комбінуємо звуки таким чином, щоб вийшов правильний варіант і програємо його. Потім потрібно призупинити таймер на деякий час (мінімум на 60 секунд). Це робиться для того, щоб годинник безупинно, протягом хвилини, не повторював час. Для того, щоб основний таймер автоматично запустився через 60 секунд необхідно запустити додатковий таймер, період якого становить >60 секунд. По спрацюванню додаткового таймера потрібно запустити основний таймер. В свою чергу основний таймер повинен вимкнути додатковий. Вони взаємовиключають один одного, тобто коли працює один — інший не працює.
  • Якщо надійшов сигнал закрити програму, видаляємо значок програми з панелі задач і закриваємо програму.

Підведемо підсумки для блоку «Обробник«. Даний блок містить в собі 2 елементи «Timer» і 1 елемент «ContextMenuStrip«.

Розглянемо головний метод даного блоку:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
private void SayTime(object sender, EventArgs e)
{
    // nullable type
    byte ?
        checking_time_10, 
        checking_time_20, 
        checking_time_30, 
        checking_time_40, 
        checking_time_50, 
        checking_time_00;
 
    checking_time_10 = File.Exists(PathSchedule + "10") ? 10 : (byte?)null;
    checking_time_20 = File.Exists(PathSchedule + "20") ? 20 : (byte?)null;
    checking_time_30 = File.Exists(PathSchedule + "30") ? 30 : (byte?)null;
    checking_time_40 = File.Exists(PathSchedule + "40") ? 40 : (byte?)null;
    checking_time_50 = File.Exists(PathSchedule + "50") ? 50 : (byte?)null;
    checking_time_00 = File.Exists(PathSchedule + "00") ? 00 : (byte?)null;
 
    if ((GetMinutes() == checking_time_10) 
        || (GetMinutes() == checking_time_20) 
        || (GetMinutes() == checking_time_30) 
        || (GetMinutes() == checking_time_40) 
        || (GetMinutes() == checking_time_50) 
        || (GetMinutes() == checking_time_00))
    {
        // stop the main timer
        timerSayTime.Enabled = false;
 
        speak_hours();
        speak_minutes();
 
        // start timer for the delayed start of the main timer
        timerForDelay.Enabled = true;
    }
}

Даний метод викликається основним таймером щосекунди. Спочатку здійснюється перевірка на існуючі файли для визначення графіку озвучування. Тут ми бачимо можливо цікаву для декого конструкцію зі знаком запитання. Тут таких конструкцій навіть декілька. В одному випадку знак запитання стоїть при об’явленні змінних. Це робиться для того, щоб була можливість присвоювати змінній null. Якби ми об’явили змінну звичайним чином, наприклад:

1
byte variable;

то такій змінній присвоїти null було б неможливо. Англійською мовою даний тип змінних називається «nullable«. Що ж стосовно іншого знаку питання, то тут використовується тернарна умовна операція. Вона являє собою аналог конструкції «if-else«. Приклад:

1)

1
2
3
4
if (true)
    a = 1;
else
    a = 0;

2)

1
a = true ? 1 : 0;

Записи 1 та 2 аналогічні один одному.

Як бачимо, в коді реалізовано те, що було описано в попередньо сформульованому алгоритмі. Для більш детального розбору варто завантажити повну версію вихідного коду (мабуть уперше так назвав сурци :).

Блок «Налаштування». В цьому блоці користувач повинен вказувати коли саме повинна відбуватись подія озвучування. Отже, як було сказано раніше, озвучування може відбуватись з кроком в 10 хвилин (0, 10, 20, 30, 40 і 50 хвилин). Створюємо 6 елементів «CheckBox» які і будуть визначати час озвучування. Для зручності, просто створюємо порожній файл з конкретним ім’ям, якщо потрібно озвучувати час, і видаляємо його, якщо озвучувати не потрібно. В такому випадку можна навіть «вручну» в файловому менеджері конфігурувати програму. На сам кінець, з цього ж таки блоку можливо викликати блок «Персоналізація«.

В результаті розробки даного блоку ми маємо отримати форму з 6 елементами «CheckBox«, елементом типу «Button» для збереження поточних налаштувань і знову ж таки елементом типу «Button» для запуску блоку персоналізації.

Основною частиною цього блоку є метод, що відповідає за вибір часу озвучування:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
private void aquaButtonSaveRepeatTime_Click(object sender, EventArgs e)
{
    // create schedule file if checkbox is checked
    if (checkBox10.Checked == true) if (!(File.Exists(PathSchedule + "10"))) { using (File.Create(PathSchedule + "10"));}
    if (checkBox20.Checked == true) if (!(File.Exists(PathSchedule + "20"))) { using (File.Create(PathSchedule + "20"));}
    if (checkBox30.Checked == true) if (!(File.Exists(PathSchedule + "30"))) { using (File.Create(PathSchedule + "30"));}
    if (checkBox40.Checked == true) if (!(File.Exists(PathSchedule + "40"))) { using (File.Create(PathSchedule + "40"));}
    if (checkBox50.Checked == true) if (!(File.Exists(PathSchedule + "50"))) { using (File.Create(PathSchedule + "50"));}
    if (checkBoxOnBegining.Checked == true) if (!(File.Exists(PathSchedule + "00"))) { using (File.Create(PathSchedule + "00"));}
 
    // if checkbox control is not checked - delete schedule file
    if (checkBox10.Checked == false) if ((File.Exists(PathSchedule + "10"))) { File.Delete(PathSchedule + "10"); }
    if (checkBox20.Checked == false) if ((File.Exists(PathSchedule + "20"))) { File.Delete(PathSchedule + "20"); }
    if (checkBox30.Checked == false) if ((File.Exists(PathSchedule + "30"))) { File.Delete(PathSchedule + "30"); }
    if (checkBox40.Checked == false) if ((File.Exists(PathSchedule + "40"))) { File.Delete(PathSchedule + "40"); }
    if (checkBox50.Checked == false) if ((File.Exists(PathSchedule + "50"))) { File.Delete(PathSchedule + "50"); }
    if (checkBoxOnBegining.Checked == false) if ((File.Exists(PathSchedule + "00"))) { File.Delete(PathSchedule + "00"); }
}

З коду можна зрозуміти, що файл з заданим ім’ям створюється або видаляється, в залежності від того «CheckBox» був чи не був «Checked«.

Блок «Персоналізація». В цьому блоці реалізовано алгоритм запису власної версії звукових файлів (надиктовування). Розглянемо, як правильно озвучувати час. Наприклад, «перша година», «десята година», «нуль годин» і т.д. отже можна створити по елементу «Button» для кожної з годин, разом повинно бути 24. Озвучування хвилин звучатиме наступним чином: «нуль хвилин», «десять хвилин», …, «п’ятдесят хвилин». Як бачимо, слово «хвилин» повторюється завжди, а отже його можна записати один раз в окремий файл. В результаті отримаємо 7 файлів: «нуль», «десять», «двадцять», «тридцять», «сорок», «п’ятдесят» і слово «хвилин».

В блоці персоналізації маємо багато елементів керування. На мій погляд, алгоритм роботи з даним блоком має бути наступним:

натискаємо кнопку з відповідним написом і в мікрофон диктуємо свою версію звукового файлу. Окрім цього, необхідно встигнути сказати фразу за відведений для неї проміжок часу (це необхідно для того, щоб в подальшому зібрана з частин фраза озвучування часу звучала цільно і природно). Стільки часу залишилось можна спостерігати в нижній частині програми по елементу «ProgressBar«. Для більш приємної роботи з даним блоком було б непогано організувати програвання записаного файлу одразу після запису. Це не повинно викликати жодних труднощів.

В результаті розробки даного блоку ми маємо отримати форму з великою кількість елементів типу «Button«, а також элементом типу «ProgressBar«.

Перейдемо до коду. Під час запуску форми ми перевіряємо на наявність необхідну для роботи директорію. Якщо директорія відсутня – створюємо її:

1
2
3
4
5
6
7
8
9
10
private void FormPersonalization_Load(object sender, EventArgs e)
{
    // create dirretory if it is doesn't exist
    if (Directory.Exists(PathSoundingMinutes) == false)
        Directory.CreateDirectory(PathSoundingMinutes);
 
    // create dirretory if it is doesn't exist
    if (Directory.Exists(PathSoundingHours) == false)
        Directory.CreateDirectory(PathSoundingHours);
}

Як ми вже зрозуміли, елементів «Button» на формі занадто багато, порядка 30 і було б безглуздям робити стільки ж само обробників для події натискання по кнопці. Замість цього зробимо один обробник, в якому будемо приймати і розшифровувати дані про те, з якого елементу керування викликано даний обробник. Звучить страхітливо, але коду тут кілька строк:

1
2
3
4
5
private void button_hours_Click(object sender, EventArgs e)
{
    Button btn = (Button)sender;
    CreateWavHours(PathSoundingHours + "" + btn.Text);
}

Змінна «sender» типу «object» тримає в собі «refference» на контрол, який викликав даний метод, тобто вказує, хто був відправником. Простим приведенням типів ми отримуємо елемент керування, в якому ми зацікавлені.

Для створення звукових файлів (запису) використовується «API» функція «mciSendStringA()» з бібліотеки «winmm.dll«. Для підключення цієї функції необхідно записати наступне:

1
2
[DllImport("winmm.dll", EntryPoint = "mciSendStringA", CharSet = CharSet.Ansi, SetLastError = true, ExactSpelling = true)]
private static extern int mciSendString(string lpstrCommand, string lpstrReturnString, int uReturnLength, int hwndCallback);

Тепер ми можемо використовувати дану функцію в нашому проекті, наприклад так:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
public void CreateWavHours(string FileName)
{
    int sampleResolution = 16;
    int sampleRate = 22050;
    int channels = 1;
 
    mciSendString("open new Type waveaudio Alias recsound", "", 0, 0);
    mciSendString("set recsound bitspersample " + sampleResolution.ToString(), "", 0, 0);
    mciSendString("set recsound samplespersec " + sampleRate.ToString(), "", 0, 0);
    mciSendString("set recsound channels " + channels.ToString(), "", 0, 0);
    mciSendString("set recsound format pcm", "", 0, 0);
    mciSendString("record recsound", "", 0, 0);
 
    Int16 i = 0;
    for (i = 0; i < 20; i++)
    {
        if (this.progressBar1.Value == 20) this.progressBar1.Value = 0;
        this.progressBar1.Value += 1;
        System.Threading.Thread.Sleep(90);
    }
 
    // stop and save
    mciSendString("save recsound " + FileName + ".wav", "", 0, 0);
    mciSendString("close recsound ", "", 0, 0);
 
    // play the recorded file
    SoundPlayer player = new SoundPlayer();
    player.SoundLocation = FileName + ".wav";
    player.PlaySync();
}

Як бачимо, спочатку ми настроюємо пристрій для запису звуку, потім записуємо і зрештою відтворюємо звук (для самоаналізу).

Блок «Про програму». Вважаю зайвим розглядати «розробку» даного блоку. Його виконання являється довільним. Обов’язковим в ньому є те, що потрібно вказати версію програми. На роботу програми в цілому це не вплине, але для розробника, а подекуди й користувача є важливим, знати версію програми.

На додачу до всього необхідно продумати, аби програма запускалась в одиничному екземплярі, адже якщо користувач запускатиме кілька програм одночасно, можуть виникнути колізії. Одна з них — одночасно спрацює кілька програм і час буде озвучений хоровим голосом, а кому це потрібно?! :) З тим, як запобігти запуск кількох програм одночасно, можна ознайомитись тут. В результаті ми отримуємо ось такий код (файл «Program.cs«):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
static void Main()
{
    string _mutexID = "Годинник говорун (Сігнаєвський Віктор Миколайович)";
 
    Application.EnableVisualStyles();
    Application.SetCompatibleTextRenderingDefault(false);
 
    // using mutex it is possible to run only one instance of the program
    Boolean _isNotRunning;
    using (Mutex _mutex = new Mutex(true, _mutexID, out _isNotRunning))
    {
        if (_isNotRunning)
        {
            // in such a way we making our main form invisible
            new FormMain();
            Application.Run();
        }
        else
        {
            MessageBox.Show(
                "Іншу копію програми вже запущено!", 
                "Увага!", 
                MessageBoxButtons.OK, 
                MessageBoxIcon.Exclamation);
        }
    }
}

Тобто в функції Main(), перед створенням головної форми ми встановили об’єкт синхронізації Mutex. Що ж робить Mutex? Для простоти розуміння це об’єкт, що містить в собі глобальну для всієї операційної системи змінну яка може бути позначена як зайнята або вільна. Тобто з різних програм ми маємо доступ до цього ж самого об’єкту і можемо перевіряти стан цієї віртуальної змінної. В даному випадку, під час запуску програми, ми перевіряємо, чи створена така змінна, чи ні. Якщо змінна вже створена повідомляємо користувача про те, що програма вже відкрита в іншому місці, якщо ж ні – ініціалізуємо Mutex об’єкт. Доречі, об’єкт повинен мати унікальний ідентифікатор. В нашому випадку це скромна фраза:

1
string _mutexID = "Годинник говорун (Сігнаєвський Віктор Миколайович)";

Іще один аспект. Ми відмовились від ідеї показувати головну форму користувачеві. В коді це можна зробити наступним чином:

1
2
3
// in such a way we making our main form invisible
new FormMain();
Application.Run();

Замість автоматично згенерованого коду:

1
Application.Run(new FormMain());

Ну от і все… Програма готова!

 

Головна частина

Завантажити вихідний код можна тут.

Завантажити скомпільований проект можна тут.

Чекаю пропозицій та зауважень від читача.