M5StackCore2 自主トレ３＜Buttonクラス＞画面遷移 カウントダウンタイマーの作成

# Buttonクラス　画面を切り替えながら、ソフトウェアの設定を行う。
<iframe width="560" height="315" src="https://www.youtube.com/embed/LsQxGLSdmZ0?si=TX93hWh2XM-gEwUa" title="YouTube video player" frameborder="0" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share" allowfullscreen></iframe>
Pageクラスを作成して、その中に、複数のButtonを所属させます。オブジェクト指向言語であるC++の特性を利用して、画面ページをクラス化、ページ単位で、画面の遷移を行い、UI操作を簡便にしました。

**Buttonクラス**の**コンストラクタ**と**デストラクタ**が画面の遷移を円滑に処理します。
コンストラクタが起動して、Buttonがdrawメソッドで描画されます。このとき、TFT液晶のテキスト描画設定が保存され、ボタン描画ためのテキスト設定に変更されます(drawメソッド内で実行されるTFT->pushState())。描画し終わったら、TFT->popState();でもとのテキスト描画設定に、戻しています。

自作した**Pageインスタンス** が、**スコープの範囲**を出ると、デストラクタが起動。PageインスタンスとButtonインスタンスが、メモリから消去されていきます。画面描画はまだ残っているので、fillscreen(BLACK)で消去しています。

メモリの確保と、解放をC言語でmallocなどを使って記述することを考えると、オブジェクト指向言語であるC＋＋の利便性は抜群です。

# 自主トレ3 カウントダウンタイマー の動作内容
メインの画面から、Menu画面に移行します。メニュー画面でカウント値の設定を指定したら、次の画面(Page1)で、PlusボタンとMinusボタンで、カウント値を決定します。OKボタンをおしたら、メイン画面に戻り、カウントダウンを始めます。

まず、Buttonｲﾝｽﾀﾝｽを理解するために、M5Core2のヘッダーファイルを、覗いてみます。

``` CPP:M5Core2.h
class M5Core2 {
 public:
  M5Core2();
  [[deprecated("It is recommended to use M5Unified Lib, which will be discontinued soon.")]] 
  void begin(bool LCDEnable = true, bool SDEnable = true,
             bool SerialEnable = true, bool I2CEnable = false,
             mbus_mode_t mode = kMBusModeOutput, bool SpeakerEnable = true);
  void update();

  void shutdown();
  int shutdown(int seconds);
  int shutdown(const RTC_TimeTypeDef &RTC_TimeStruct);
  int shutdown(const RTC_DateTypeDef &RTC_DateStruct,
               const RTC_TimeTypeDef &RTC_TimeStruct);

  // LCD
  M5Display Lcd;

  // Power
  AXP Axp;

  // Touch
  M5Touch Touch;

  // Buttons (global button and gesture functions)
  M5Buttons Buttons;

  // Default "button" that gets events where there is no button.
  Button background = Button(0, 0, TOUCH_W, TOUCH_H, true, "background");

  // Touch version of the buttons on older M5stack cores, below screen
  Button BtnA = Button(10, 240, 110, 40, true, "BtnA");
  Button BtnB = Button(130, 240, 70, 40, true, "BtnB");
  Button BtnC = Button(230, 240, 80, 40, true, "BtnC");

  MPU6886 IMU;

  // I2C
  CommUtil I2C;

  RTC Rtc;

  Speaker Spk;

  /**
         * Functions have been moved to Power class for compatibility.
         * These will be removed in a future release.
         */
  void setPowerBoostKeepOn(bool en) __attribute__((deprecated));
  void setWakeupButton(uint8_t button) __attribute__((deprecated));
  void powerOFF() __attribute__((deprecated));

 private:
  bool isInited;
};

extern M5Core2 M5;
```
液晶全体がbackgroundという、Buttonクラスインスタンスとしてオブジェクト化されています。それと、画面下のAボタン、Bボタン、Cボタンの3つが、インスタンス化されています。Buttonクラスの定義を見てみますと、
```CPP:
class Button : public Zone {
 public:
  static std::vector<Button*> instances;
   ＜略＞
};
```
std::vectorというC＋＋の動的配列を扱うコンテナクラスがstaticメンバー変数として定義されています。

Buttonクラスのコンストラクタが起動すると、作られたButtonインスタンスは、このvectorにpush_back、つまり、動的配列の末尾に追加されていきます。

最初のvectorのsizeは、４個(backgroundと、A,B,Cボタン）ということになります。

``` CPP:M5Button.cpp
＜略＞
void Button::init() {
  _state = _tapWait = _pressing = _manuallyRead = false;
  _time = _lastChange = _pressTime = millis();
  _hold_time = -1;
  _textFont = _textSize = 0;
  _freeFont = nullptr;
  drawFn = nullptr;
  _compat = 0;
  drawZone = Zone();
  tapTime = TAP_TIME;
  dbltapTime = DBLTAP_TIME;
  longPressTime = LONGPRESS_TIME;
  repeatDelay = REPEAT_DELAY;
  repeatInterval = REPEAT_INTERVAL;
  strncpy(_label, _name, 16);
  if (_pin != 0xFF) pinMode(_pin, INPUT_PULLUP);
  instances.push_back(this);
  draw();
}
＜略＞　
```

# 今回の実行コード

``` CPP:myButton1.hpp
#ifndef __MYBUTTON1_HPP__
#define __MYBUTTON1_HPP__

#include <M5Core2.h>

#define BUTTONSIZE_H 150
#define BUTTONSIZE_V 50
#define BUTTONGAP_H 70


/*********************************************/
// Menuページ                            
/*********************************************/

//menuページのコールバック関数
//静的に確保する必要があるので、クラス外宣言。
extern void menuBtnResetCallBack(Event& e);
extern void menuBtnSetPeriodCallBack(Event& e);
extern void menuBtnSetCOMMANDCallBack(Event& e);
extern void menuBtnReturnCallBack(Event& e);


/// @brief Menuページクラス
class PageMenu
{   
    public:
    PageMenu();
    ~PageMenu();
    void SetButtonInfo(void);//ボタンの設定
    public:
    //ボタンカラー(背景色、テキスト色、アウトライン色)
    //menuクラス Resetボタン
    Button menuBtnReset      = Button( 10,               20,BUTTONSIZE_H, BUTTONSIZE_V,false, 
                                    "RESET",{RED,WHITE,TFT_TRANSPARENT},{MAGENTA,WHITE,TFT_TRANSPARENT},MC_DATUM,0,0,10);
    //menuクラス Period設定ボタン
    Button menuBtnSetPeriod    = Button( 10, 20+BUTTONGAP_H*1,BUTTONSIZE_H, BUTTONSIZE_V,false,    
                                    "PERIOD",{GREEN,DARKGREEN,TFT_TRANSPARENT},{DARKGREEN,WHITE,TFT_TRANSPARENT},MC_DATUM,0,0,10);
    //menuクラス COMMANDボタン
    Button menuBtnSetCOMMAND = Button( 10, 20+BUTTONGAP_H*2,BUTTONSIZE_H, BUTTONSIZE_V,false, 
                                    "COMMAND",{YELLOW,DARKGREEN,TFT_TRANSPARENT},{ORANGE,WHITE,TFT_TRANSPARENT},MC_DATUM,0,0,10);
    //menuクラス Returnボタン
    Button menuBtnReturn     = Button(170, 40+BUTTONGAP_H*2,BUTTONSIZE_H, BUTTONSIZE_V,false,   
                                    "RETURN",{CYAN,BLUE,TFT_TRANSPARENT},{BLUE,WHITE,TFT_TRANSPARENT},MC_DATUM,0,0,10);
    public:
    static bool close;    //pageを閉じる
    static int nextPageNumber; //次ページ番号
    static String selectedMenuText; //選択したMenu文字列(次ページに渡す)
};


/*********************************************/
// Page1  値設定ページ                     
/*********************************************/

//page1ページのコールバック関数
//静的に確保する必要があるので、クラス外宣言。
extern void page1BtnPlusCallBack(Event& e);
extern void page1BtnMinusCallBack(Event& e);
extern void page1BtnOKCallBack(Event& e);
extern void page1BtnCancelCallBack(Event& e);

//Page1クラス
class Page1
{
    public:
    Page1();
    ~Page1();
    public:
    //page1　＋ボタン
    Button page1BtnPlus  = Button( 10,20+BUTTONGAP_H*1, BUTTONSIZE_H, BUTTONSIZE_V,false, 
                        "+:Plus",{CYAN,BLUE,TFT_TRANSPARENT},{BLUE,WHITE,TFT_TRANSPARENT},MC_DATUM,0,0,10);
    //page1　-ボタン
    Button page1BtnMinus = Button( 20+BUTTONSIZE_H,20+BUTTONGAP_H*1, BUTTONSIZE_H, BUTTONSIZE_V,false, 
                        "-:Minus",{CYAN,BLUE,TFT_TRANSPARENT},{BLUE,WHITE,TFT_TRANSPARENT},MC_DATUM,0,0,10);
    //page1　OKボタン
    Button page1BtnOK    = Button( 10, 20+BUTTONGAP_H*2, BUTTONSIZE_H, BUTTONSIZE_V,false, 
                        "OK",{YELLOW,DARKGREEN,TFT_TRANSPARENT},{ORANGE,WHITE,TFT_TRANSPARENT},MC_DATUM,0,0,10);
    
    //page1 Cancelボタン
    Button page1BtnCancel= Button( 20+BUTTONSIZE_H, 50+BUTTONGAP_H*2,BUTTONSIZE_H, BUTTONSIZE_V,false, 
                        "CANCELL",{LIGHTGREY,DARKGREY,TFT_TRANSPARENT},{DARKGREY,LIGHTGREY,TFT_TRANSPARENT},MC_DATUM,0,0,10);

    static bool close;//Page1を閉じる
    static bool OK;   //OKボタン押下
    static int number;//カウンタ値
    static bool enCountDown;//カウント開始フラグ
};

/// @brief 汎用ボタン情報設定
/// @param bt Buttonクラスインスタンスの参照
/// @param size テキストサイズ
/// @param freeFont_ フォント指定参照
/// @param fn コールバック関数
/// @param eventMask コールバックイベントマスク(ex.E_RELEASE)
extern void BtnInfoSet(Button &bt, int size, 
                const GFXfont *freeFont_, EventHandlerCallback fn
                ,uint16_t eventMask = (uint16_t)4095U);

#endif
```

```CPP:myButton1.cpp
#include "myButton1.hpp"

/*********************************************/
// Menuページ                            
/*********************************************/

//PageMenuクラス staticメンバ変数宣言
bool PageMenu::close=false;           //Menuを閉じる
String PageMenu::selectedMenuText=""; //次ページに表示する文字列
int PageMenu::nextPageNumber=0;       //次ページ展開するとき使用。

//Menuページ Resetボタン処理
void menuBtnResetCallBack(Event& e)
{
    PageMenu::selectedMenuText="RESET";
    PageMenu::close=true;
}

//Menuページ Periodボタン処理
void menuBtnSetPeriodCallBack(Event& e)
{
    PageMenu::selectedMenuText="Period";
    PageMenu::nextPageNumber=1;
    PageMenu::close=true;
}

//Menuページ　COMMANDボタン処理
void menuBtnSetCOMMANDCallBack(Event& e)
{
    PageMenu::selectedMenuText="COMMAND";
    PageMenu::close=true;
}

//Menuページ Returnボタン処理
void menuBtnReturnCallBack(Event& e)
{
    PageMenu::nextPageNumber=0;
    PageMenu::close=true;
}

//PageMenuクラスコンストラクタ
PageMenu::PageMenu()
{
    Serial.println("PageMenu Construction");
    SetButtonInfo();
    PageMenu::nextPageNumber=0;
    PageMenu::close=false;
}

//PageMenuクラスデストラクタ
PageMenu::~PageMenu()
{
    Serial.println("PageMenu destruction");
}

/// ボタンの設定 
void PageMenu::SetButtonInfo(void)
{
    BtnInfoSet(menuBtnReset,1,&FreeSans12pt7b,&menuBtnReturnCallBack,E_RELEASE);
    BtnInfoSet(menuBtnSetPeriod,1,&FreeSans12pt7b,&menuBtnSetPeriodCallBack,E_RELEASE);
    BtnInfoSet(menuBtnSetCOMMAND,1,&FreeSans12pt7b,&menuBtnSetCOMMANDCallBack,E_RELEASE);
    BtnInfoSet(menuBtnReturn,1,&FreeSans12pt7b,&menuBtnReturnCallBack,E_RELEASE);
}

/*********************************************/
// Page1   カウンタ値設定ページ                       
/*********************************************/

//Page1クラス static メンバー変数
bool Page1::close=false;      //Page1を閉じる
bool Page1::OK=false;         //OKボタン押下
int  Page1::number=0;         //カウンタ値設定
bool Page1::enCountDown=false;//カウントダウン開始

//page1クラス Plusボタン処理
void page1BtnPlusCallBack(Event& e)
{
    Page1::number++;
    TFT->setCursor(150,50);
    TFT->setTextSize(2);
    TFT->fillRect(140,20,160,40,BLACK);
    TFT->drawRect(140,20,160,40,YELLOW);
    TFT->printf("Num:%4d",Page1::number);
}

//page1クラス Minusボタン処理
void page1BtnMinusCallBack(Event& e)
{
    Page1::number--;
    TFT->setCursor(150,50);
    TFT->setTextSize(2);
    TFT->fillRect(140,20,160,40,BLACK);
    TFT->drawRect(140,20,160,40,YELLOW);
    TFT->printf("Num:%4d",Page1::number);
}

//page1クラス OKボタン処理
void page1BtnOKCallBack(Event& e)
{
    Page1::OK=true;
    Page1::close=true;
    if(Page1::number>0);
    {
        Page1::enCountDown=true;
    }
}

//page1クラス Cancelボタン処理
void page1BtnCancelCallBack(Event& e)
{
    Page1::enCountDown=false;
    Page1::OK=false;
    Page1::close=true;
}

//Page1クラスコンストラクタ
//カウンタ値の設定
Page1::Page1()
{
    Serial.println("Page1 Construction");
    Page1::OK=false;//OKボタン押下
    Page1::enCountDown=false;//カウントダウン開始フラグ
    Page1::close=false;//Page1閉じる

    //前ページの設定文字列表示
    TFT->setCursor(3,50);
    TFT->setTextSize(2);
    TFT->printf("%s",PageMenu::selectedMenuText);
    //カウンタ値の表示
    TFT->setCursor(150,50);
    TFT->setTextSize(2);
    TFT->drawRect(140,20,160,40,YELLOW);
    TFT->printf("Num:%4d",Page1::number);
    //ボタン設定、表示
    BtnInfoSet(page1BtnPlus,1,&FreeSans12pt7b,&page1BtnPlusCallBack,E_RELEASE);
    BtnInfoSet(page1BtnMinus,1,&FreeSans12pt7b,&page1BtnMinusCallBack,E_RELEASE);
    BtnInfoSet(page1BtnOK,1,&FreeSans12pt7b,&page1BtnOKCallBack,E_RELEASE);
    BtnInfoSet(page1BtnCancel,1,&FreeSans12pt7b,&page1BtnCancelCallBack,E_RELEASE);
}

//Page1クラス デストラクタ
Page1::~Page1()
{
    Serial.println("Page1 destruction");
}

//*****************************************
//utilities
//*****************************************

/// @brief Buttonクラス情報設定関数
/// @param bt Buttonｲﾝｽﾀﾝｽ
/// @param size TextSize
/// @param freeFont_ freefonttype
void BtnInfoSet(Button &bt, int size, const GFXfont *freeFont_, 
            EventHandlerCallback fn,uint16_t eventMask)
{
    bt.setFreeFont(freeFont_); 
    bt.setTextSize(size);
    bt.addHandler(fn,eventMask);
    bt.draw();
}
```

``` C++:main.cpp
#include <M5Core2.h>
#include "myButton1.hpp"

//Buttonクラスの練習帳
//Page展開

#define LCDWIDE 320
#define LCDHEIGHT 240
#define ZONE_WIDE 130
#define ZONE_HEIGHT 50
#define ZONE_WIDE_HALF 50
#define ZONE_HEIGHT_HALF 25
TFT_eSprite canvas = TFT_eSprite(&M5.Lcd);

void Page0Paint(void);

void setup()
{
    M5.begin();
    canvas.createSprite(320, 240);  //Create a 320x240 canvas.
    canvas.setColorDepth(8);        // Set color depth.     
    pinMode(32,OUTPUT);             //ブザー
    Page1::enCountDown=false;
    Page1::number=0;
    Page1::OK=false;
    Page0Paint();                   //画面描画
}

static int counter;

void loop()
{
   M5.update();
   
   
   if(Page1::enCountDown)
   {//カウントダウン中----------------------------
      digitalWrite(32,1);
      delay(200);
      digitalWrite(32,0);
      counter--;
      if(counter==0)
      {
        Page1::enCountDown=false;
        Page0Paint();
        canvas.setCursor(70,220-20);
        canvas.setTextColor(RED);
        canvas.println("Time UP!!");
        canvas.pushSprite(0,0);
        do
        {
          //for(int j=0; j<4; j++)
          {
            digitalWrite(32,1);
            delay(50);
            digitalWrite(32,0);
            delay(50);
          }          
          M5.update();
          if(M5.BtnC.wasPressed())
          {
            break;
          }
        }while(1);
        counter=Page1::number;
      }
   }

   //ボタンA Menu表示(PageMenuを開く)
   if(M5.BtnA.wasPressed())
   {
      canvas.fillScreen(BLACK);
      canvas.pushSprite(0,0);
      PageMenu pmenu;
      while(1)
      {
        M5.update();
        if(PageMenu::close)
        {
            //Serial.println("Close");
            break;
        }
        delay(10);
      }
      canvas.fillScreen(BLACK);
      canvas.pushSprite(0,0);
      //Page1の処理
      if(PageMenu::nextPageNumber==1)
      {//Periodの設定　
        Page1 page1;
        while(1)
        {
          M5.update();
          if(Page1::close)
          {
            break;
          }
          delay(10);
        }
        if(Page1::enCountDown)
        {
          counter=Page1::number;
        }
      }
      canvas.fillScreen(BLACK);
      canvas.pushSprite(0,0);
   }
   
   if(M5.BtnB.wasPressed())
   {
      if(Page1::number>0)
      {        
        Page1::enCountDown=true;
      }
   }

   Page0Paint();
   if(Page1::enCountDown)
    delay(1000);
   else
    delay(1);
}

void Page0Paint(void)
{
  canvas.fillScreen(BLACK);
  canvas.setFreeFont(&FreeSans12pt7b); 
  canvas.setTextDatum(MC_DATUM);  
  canvas.setTextSize(0);
  canvas.setCursor(0,20);    
  canvas.setTextColor(RED);
  canvas.println("M5StackCore2");
  canvas.setTextColor(ORANGE);
  canvas.println("Button Class Test");
  canvas.setTextColor(GREEN);
  canvas.println("Some pages have buttons.");

  if(Page1::enCountDown)
  {
    canvas.setCursor(50,220-20);
    canvas.setFreeFont(&Orbitron_Light_32);
    canvas.setTextColor(GREEN);
    canvas.println("Count Down");
  }

  canvas.setFreeFont(&FreeSans12pt7b);
  canvas.setTextColor(CYAN,LIGHTGREY);
  canvas.setCursor(30,230);canvas.print("SET");
  canvas.setCursor(110,230);canvas.print("START");
  canvas.setCursor(230,230);canvas.print("STOP");

  canvas.fillRect(70,110-10,180,60,BLUE);
  canvas.setCursor(80,160-10);
  canvas.setTextColor(WHITE);
  for(int w=0; w<8; w++)
  {
    canvas.drawRect(70-w,110-w-10,180+w*2,60+w*2,ORANGE);
  }
  canvas.setFreeFont(&Orbitron_Light_32);
  canvas.printf("Num:%4d",counter);
  canvas.pushSprite(0,0);
}
```