mieki256's diary



2024/01/01(月) [n年前の日記]

#1 [nitijyou] あけましておめでとうございます

あけましておめでとうございます。本年もよろしくお願いいたします。

#2 [prog] bin2cについて

C/C++を使って、Windows用の実行形式ファイル(exeファイル)に、png画像のバイナリをそのまま含める方法について調べてた。

リソースファイルを使う方法 :

一般的には、リソースファイル(.rc)というものを用意して、実行形式ファイル内にバイナリを同梱させる場合が多そうな印象を受けた。

リソースファイル内にpng画像のファイル名やIDを書いておいて、gcc の場合は windres というツールを使うことで、リソースファイル(.rc)からオブジェクトファイル(.o)を生成。そのオブジェクトファイルをリンカに渡して実行形式を作る。

リソースファイル内のバイナリを取り出すには、FindResource()、LoadResource() 等の関数を使う。

_リソースのPNG画像を使う方法
_FAQ9-2
_バイナリリソースの追加-Windowsプログラミング実験室-
_カスタムリソース

ただ、関連情報を探しているうちに、bin2c というツールがあることを知った。

bin2cを使う方法 :

bin2ch は、バイナリファイルを読み込んで、C/C++ の unsigned char 配列の形でテキスト出力するツールらしい。考えてみれば、そういうやり方でもバイナリを含めることはできるなと…。

この bin2c というツール、標準的なツールとして存在しているわけではなさそうで。やってることが単純というか、プログラマーなら誰でも書ける処理のせいか、ツール名は同じでも、色んな人が実装して公開している模様。

_bin2c download | SourceForge.net
_gwilymk/bin2c: A very simple utility for converting a binary file to a c source file
_j.eng's site: hxtools
_What is Bin2c

中には Python で書かれたものもあった。

_delimitry/bin2c: Simple tool for creating C array from a binary file

何にせよ、bin2c を使うやり方でも、目的は果たせそうではあるなと…。

使ってみた :

SourceForge で公開されている版を入手して使ってみた。

_bin2c download | SourceForge.net
_bin2c - Browse /1.1 at SourceForge.net

bin2c-1.1.zip を入手して解凍すると、中には以下の3つのファイルが入っている。
bin2c.c
bin2c.exe
readme.txt

bin2c.exe が実行ファイル。

texture.png を渡して、texture.h を生成してみる。
bin2c.exe -o texture.h texture.png

以下のような感じの .h が得られた。
/* Generated by bin2c, do not edit manually */

/* Contents of file texture.png */
const long int texture_png_size = 103208;
const unsigned char texture_png[103208] = {
    0x89, 0x50, 0x4E, 0x47, 0x0D, 0x0A, 0x1A, 0x0A, 0x00, 0x00, 0x00, 0x0D, 0x49, 0x48, 0x44, 0x52,
    0x00, 0x00, 0x02, 0x00, 0x00, 0x00, 0x02, 0x00, 0x08, 0x06, 0x00, 0x00, 0x00, 0xF4, 0x78, 0xD4,
    0xFA, 0x00, 0x00, 0x20, 0x00, 0x49, 0x44, 0x41, 0x54, 0x78, 0xDA, 0xEC, 0xBD, 0x79, 0xBC, 0x5D,
// ...
    0x1A, 0xBE, 0xFD, 0x3F, 0x10, 0x01, 0xD1, 0x1A, 0x6F, 0xDC, 0xEF, 0xE1, 0x00, 0x00, 0x00, 0x00,
    0x49, 0x45, 0x4E, 0x44, 0xAE, 0x42, 0x60, 0x82
};

「#include "texture.h"」とでも書いてやれば、このバイナリを含めることができるのではないかなと…。たぶん。まだ試してないけど。

2024/01/02(火) [n年前の日記]

#1 [prog] メモリ上のpngバイナリをOpenGLのテクスチャとして利用

昨日、bin2c というツールを使えば、何かしらのバイナリファイルをC言語の unsigned char 配列の形(.h)に変換できると知った。

_mieki256's diary - bin2cについて

その unsigned char 配列を .c に include すれば、実行形式 (.exe) にpngバイナリを含めてしまうこともできるはず。

そんなわけで、実行形式(.exe)に含まれたpngバイナリを OpenGL のテクスチャとして利用できるのか試してみた。もし、それができれば、.exe を1つ配布するだけで画像描画をするプログラムを配布することができるなと…。わざわざ .exe と一緒に .png も配布しなくて済む。例えばスクリーンセーバの類を作って配布する際などは、少しだけ都合が良いかもしれない。

使用言語はC言語。環境は Windows10 x64 22H2 + MinGW (gcc 6.3.0) or MSYS2 (gcc 13.2.0)。

今回、メモリ上のpngバイナリを読み込んで展開しなければいけないけれど、そこでlibpngライブラリ、もしくは stbライブラリを使えるのか試してみた次第。

必要なライブラリ :

以後のサンプルをビルドするには、freeglut、libpng、zlib(libz) が必要。

MinGW上での導入作業は以前のメモを参考に。

_mieki256's diary - MinGWでfreeglutを使ってみた
_mieki256's diary - MinGW上でzlibやlibpngをビルドしてみた

MSYS2 の場合はパッケージが用意してあるので、pacman -Ss xxxx で検索して、pacman -S xxxx でインストールすればいい。
pacman -S mingw-w64-x86_64-freeglut
pacman -S mingw-w64-x86_64-libpng
pacman -S mingw-w64-x86_64-zlib

pacman -S mingw-w64-i686-freeglut
pacman -S mingw-w64-i686-libpng
pacman -S mingw-w64-i686-zlib

余談。pacman -Ss stb で検索して気づいたけれど、MSYS2 の場合、stbライブラリもパッケージとして用意されてるっぽいなと…。
mingw-w64-x86_64-stb
mingw-w64-i686-stb

_Package: mingw-w64-x86_64-stb - MSYS2 Packages

include/stb/stb*.h という形でインストールされるらしい。ということは、「#include <stb/stb_image.h>」といった形で include して利用できるのだろうか? 試してないから分からないけど。

libpngを使う事例 :

libpngを使えば、png画像を開いて画像情報を取り出すことができる。

libpng の使用サンプルは、一般的に、ストレージ上のpng画像ファイル名を渡して、ファイルポインタでファイルを読み込んで処理をしていく事例がほとんどだけど。ファイルから読み込むあたりの処理を、別途関数で書いて、その関数を libpng に設定してやることで、メモリ上に存在するpngバイナリを読み込んで処理できるらしい。その方法については、以下のページが参考になった。ありがたや。

_008 PNGテクスチャの読み込み [stepism]
_C言語 メモリ上にある、PNGデータをlibpngを介して読み込むサンプル。 - うーびのメモ


まずは、bin2c を使って、png画像を .h に変換する。bin2c は、SourceForge で公開されている版、bin2c-1.1.zip を利用させてもらった。

_bin2c download | SourceForge.net

bin2c-1.1.zip を解凍すると、中に bin2c.exe というファイルがあるので、コレを利用する。

_texture.png を、texture.h に変換する。
.\bin2c.exe -o texture.h texture.png

texture.h が得られた。

_texture.h

コレを、「#include "texture.h"」で include してやれば、exeファイルにpngバイナリを含めることができるはず…。


OpenGL でテクスチャを描画するサンプルを作成。以下のページで紹介されているサンプルを参考にさせてもらった。

_PNGファイルをOpenGLで扱う話

以下が、メモリ上(.exeファイル内)にあるpngバイナリを、libpngで処理して、OpenGL のテクスチャとして描画するサンプル。

_02_loadpng_draw.c
// OpenGL + libpng sample. PNG image is in memory

#include <stdio.h>
#include <stdlib.h>
#include <GL/freeglut.h>
#include <png.h>

// png image binary
#include "texture.h"

GLuint texture;
int offset = 0;

// Callback function for reading data in libpng
void ReadEndProcess(png_structp _pPng, png_bytep _buf, png_size_t _size)
{
  unsigned char* p = (unsigned char*)png_get_io_ptr(_pPng);
  memcpy(_buf, p + offset, _size);
  offset += _size;
}

/**
 * Load texture from png image on memory
 *
 * @param[in] _pngData png binary on memory
 * return GLuint OpenGL texture ID. if 0, process fails
 */
GLuint createTextureFromPngInMemory(const unsigned char* _pngData)
{
  GLuint texture;
  png_structp png_ptr = NULL;
  png_infop info_ptr = NULL;

  unsigned int width, height;
  int depth, colorType, interlaceType;

  int rowSize;
  unsigned char *data;

  // PNG file ?
  if (png_sig_cmp(_pngData, 0, 8) != 0)
    {
      fprintf(stderr, "createTextureFromPngInMemory() : Not png file binary");
      return 0;
    }

  // create png read struct
  png_ptr = png_create_read_struct(PNG_LIBPNG_VER_STRING, NULL, NULL, NULL);

  if (png_ptr == NULL)
    {
      return 0;
    }

  // create png information struct
  info_ptr = png_create_info_struct(png_ptr);

  if (info_ptr == NULL)
    {
      png_destroy_read_struct(&png_ptr, NULL, NULL);
      return 0;
    }

  // set read data callback function
  png_set_read_fn(png_ptr, (png_voidp)_pngData, ReadEndProcess);
  offset = 8;

  // If error, jump here
  if (setjmp(png_jmpbuf(png_ptr)))
    {
      // Release allocated memory
      png_destroy_read_struct(&png_ptr, &info_ptr, NULL);
      return 0;
    }

  // Skip identification area (8 bytes) in png files
  png_set_sig_bytes(png_ptr, 8);

  // Load image information
  // get width, height, bit depth, color type, interlace mode
  png_read_info(png_ptr, info_ptr);
  png_get_IHDR(png_ptr, info_ptr, &width, &height, &depth, &colorType, &interlaceType, NULL, NULL);

  // support RGB/RGBA only
  if (colorType != PNG_COLOR_TYPE_RGB && colorType != PNG_COLOR_TYPE_RGBA)
    {
      fprintf(stderr, "createTextureFromPngInMemory() : Supprted color type are RGB/RGBA only.");
      png_destroy_info_struct(png_ptr, &info_ptr);
      png_destroy_read_struct(&png_ptr, NULL, NULL);
      return 0;
    }

  // not support interlace
  if (interlaceType != PNG_INTERLACE_NONE)
    {
      fprintf(stderr, "createTextureFromPngInMemory() : Interlace image is not supprted.");
      png_destroy_info_struct(png_ptr, &info_ptr);
      png_destroy_read_struct(&png_ptr, NULL, NULL);
      return 0;
    }

  // calc memory size
  rowSize = png_get_rowbytes(png_ptr, info_ptr);
  data = malloc(rowSize * height);

  // read pixel
  for (int i = 0; i < height; i++)
    {
      png_read_row(png_ptr, &data[i * rowSize], NULL);
    }

  png_read_end(png_ptr, info_ptr);

  // create OpenGL texture
  glGenTextures(1, &texture);

  // select texture
  glBindTexture(GL_TEXTURE_2D, texture);

  // set texture from PNG image
  glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA, width, height, 0, GL_RGBA, GL_UNSIGNED_BYTE, data);

  // Release allocated all memory
  free(data);
  png_destroy_info_struct(png_ptr, &info_ptr);
  png_destroy_read_struct(&png_ptr, NULL, NULL);

  return texture;
}

// draw OpenGL
void display()
{
  glClear(GL_COLOR_BUFFER_BIT);

  glColor4f(1.0f, 1.0f, 1.0f, 1.0f);

  // enable texture
  glEnable(GL_TEXTURE_2D);
  glBindTexture(GL_TEXTURE_2D, texture);

  // draw polygon
  glBegin(GL_QUADS);

  GLfloat v = 0.85f;

  glTexCoord2f(0.0, 0.0); // set texture u, v
  glVertex2f(-v, v);

  glTexCoord2f(0.0, 1.0);
  glVertex2f(-v, -v);

  glTexCoord2f(1.0, 1.0);
  glVertex2f(v, -v);

  glTexCoord2f(1.0, 0.0);
  glVertex2f(v, v);

  glEnd();

  // disable texture
  glDisable(GL_TEXTURE_2D);

  glFlush();
}

// Keyboard callback function
void keyboard(unsigned char key, int x, int y)
{
  switch (key)
    {
      case '\x1B':
      case 'q':
        // Exit on escape or 'q' key press
        glutLeaveMainLoop();
        // exit(EXIT_SUCCESS);
        break;
    }
}

int main(int argc, char **argv)
{
  glutInit(&argc, argv);

  glutInitDisplayMode(GLUT_RGBA);
  glutInitWindowSize(1280, 720);
  glutCreateWindow("OpenGL Texture Example");

  // create OpenGL texture from PNG image
  texture = createTextureFromPngInMemory((void *)&texture_png);

  if (!texture)
    {
      fprintf(stderr, "Failed create texture\n");
      exit(1);
    }

  glClearColor(0.2f, 0.4f, 0.8f, 1.0f);

  glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR);
  glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);

  glutKeyboardFunc(keyboard);
  glutDisplayFunc(display);

  glEnable(GL_BLEND);
  glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA);

  glutSetOption(GLUT_ACTION_ON_WINDOW_CLOSE, GLUT_ACTION_GLUTMAINLOOP_RETURNS);

  glutMainLoop();

  return EXIT_SUCCESS;
}
  • #include "texture.h" を書くことで、exeファイル内にpng画像のバイナリを含めてしまう。
  • pngバイナリは、texture_png という名前の unsigned char 配列になっている。
  • ReadEndProcess(png_structp, png_bytep, png_size_t) が、ファイルではなくメモリ上から読み込ませるためのコールバック関数。この関数を png_set_read_fn() に渡してやることで、「ファイルポインタじゃなくてこっちの関数を使ってデータを読み込め」と指示している。
  • GLuint createTextureFromPngInMemory(const unsigned char*) が、pngバイナリの先頭アドレスを渡すとOpenGLのテクスチャとして設定してくれる関数。


Makefile は以下。

_Makefile
# MinGW

02_loadpng_draw.exe: 02_loadpng_draw.c texture.h Makefile
    gcc 02_loadpng_draw.c -o 02_loadpng_draw.exe -D FREEGLUT_STATIC -lfreeglut_static -lopengl32 -lwinmm -lgdi32 -lpng -lz

texture.h: texture.png
    .\bin2c.exe -o texture.h texture.png

.PHONY: clean
clean:
    rm -f 02_loadpng_draw.exe
    rm -f texture.h
    rm -f *.o

make と打てばビルドできるけど…。要するに以下を実行すれば、gcc で .c をコンパイル&リンクして、実行形式 02_loadpng_draw.exe が得られる。
gcc 02_loadpng_draw.c -o 02_loadpng_draw.exe -D FREEGLUT_STATIC -lfreeglut_static -lopengl32 -lwinmm -lgdi32 -lpng -lz
  • -o hoge.exe で出力ファイル名を指定。
  • -lxxxx で、libxxxx.lib ライブラリをリンク。
  • -D FREEGLUT_STATIC で、freeglut をスタティックリンクするように指示しているらしい。

02_loadpng_draw.exe を実行すると、以下のウインドウが表示された。

02_loadpng_draw_ss.png


たしかに、exeファイル内に含まれているpngバイナリを、libpngで読み込んで、OpenGLのテクスチャとして利用することができた。

余談。MSYS2 (gcc 13.2.0) を使う場合は、以下の Makefile になった。

_Makefile.msys2
# MSYS2

02_loadpng_draw.exe: 02_loadpng_draw.c texture.h Makefile.msys2
    gcc 02_loadpng_draw.c -o 02_loadpng_draw.exe -D FREEGLUT_STATIC -lfreeglut -lopengl32 -lwinmm -lgdi32 -lpng -lz -static

texture.h: texture.png
    .\bin2c.exe -o texture.h texture.png

.PHONY: clean
clean:
    rm -f 02_loadpng_draw.exe
    rm -f texture.h
    rm -f *.o

make -f xxxx と打てば、xxxx を Makefile として利用できる。
make -f Makefile.msys2

stbライブラリを使う事例 :

画像読み込みを行うライブラリは色々あるけれど、stbライブラリという、ちょっと変わったライブラリがある。ヘッダーファイル(.h)を include するだけで各機能が使えてしまうライブラリで…。ヘッダーファイルをコピーしてきて、.c と一緒に置いて include するだけなので、導入が圧倒的に楽。

この stbライブラリを利用して、メモリ上にあるpngバイナリを利用できないか試してみた。今回は、画像の読み込みだけができればいいので、stb_image.h のみを入手して、.c と同じ場所に置いた。

_nothings/stb: stb single-file public domain libraries for C/C++
_stb/stb_image.h at master - nothings/stb


_bin2c を使って、 _png画像 texture.png_texture.h に変換。
.\bin2c.exe -o texture.h texture.png


以下が、stb(stb_image.h)を使って、exeに含まれているpngバイナリを、OpenGLのテクスチャとして利用するサンプル。

_03_loadpngwithstb.c
// OpenGL + stb sample. PNG image is in memory

#include <stdio.h>
#include <stdlib.h>
#include <GL/freeglut.h>

// use stb library
#define STB_IMAGE_IMPLEMENTATION
#include "stb_image.h"

// include png image binary
#include "texture.h"

GLuint texture;

/**
 * Load texture from png image on memory
 *
 * @param[in] _pngData png binary on memory
 * @param[in] _pngDataLen png binary size
 * return GLuint OpenGL texture ID. if 0, process fails
 */
GLuint createTextureFromPngInMemory(const unsigned char* _pngData, int _pngLen)
{
  GLuint texture;
  int width = 0, height = 0, bpp = 0;
  unsigned char *data = NULL;

  data = stbi_load_from_memory(_pngData, _pngLen, &width, &height, &bpp, 4);
  
  // create OpenGL texture
  glGenTextures(1, &texture);
  glBindTexture(GL_TEXTURE_2D, texture);
  glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA, width, height, 0, GL_RGBA, GL_UNSIGNED_BYTE, data);

  // Release allocated all memory
  stbi_image_free(data);

  return texture;
}

// draw OpenGL
void display()
{
  glClear(GL_COLOR_BUFFER_BIT);

  glColor4f(1.0f, 1.0f, 1.0f, 1.0f);

  // enable texture
  glEnable(GL_TEXTURE_2D);
  glBindTexture(GL_TEXTURE_2D, texture);

  // draw polygon
  glBegin(GL_QUADS);

  GLfloat v = 0.85f;

  glTexCoord2f(0.0, 0.0); // set texture u, v
  glVertex2f(-v, v);

  glTexCoord2f(0.0, 1.0);
  glVertex2f(-v, -v);

  glTexCoord2f(1.0, 1.0);
  glVertex2f(v, -v);

  glTexCoord2f(1.0, 0.0);
  glVertex2f(v, v);

  glEnd();

  // disable texture
  glDisable(GL_TEXTURE_2D);

  glFlush();
}

// Keyboard callback function
void keyboard(unsigned char key, int x, int y)
{
  switch (key)
    {
      case '\x1B':
      case 'q':
        // Exit on escape or 'q' key press
        glutLeaveMainLoop();
        // exit(EXIT_SUCCESS);
        break;
    }
}

int main(int argc, char **argv)
{
  glutInit(&argc, argv);

  glutInitDisplayMode(GLUT_RGBA);
  glutInitWindowSize(1280, 720);
  glutCreateWindow("OpenGL Texture Example");

  // create OpenGL texture from PNG image
  texture = createTextureFromPngInMemory((void *)&texture_png, texture_png_size);

  if (!texture)
    {
      fprintf(stderr, "Failed create texture\n");
      exit(1);
    }

  glClearColor(0.2f, 0.4f, 0.8f, 1.0f);

  glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR);
  glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);

  glutKeyboardFunc(keyboard);
  glutDisplayFunc(display);

  glEnable(GL_BLEND);
  glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA);

  glutSetOption(GLUT_ACTION_ON_WINDOW_CLOSE, GLUT_ACTION_GLUTMAINLOOP_RETURNS);

  glutMainLoop();

  return EXIT_SUCCESS;
}

stb_image.h を利用するときは、ファイルの最初のほうで、以下を記述する。
#define STB_IMAGE_IMPLEMENTATION
#include "stb_image.h"

メモリ上にあるpngバイナリの読み込みは、以下の行で処理している。
  data = stbi_load_from_memory(_pngData, _pngLen, &width, &height, &bpp, 4);
  • _pngData : pngバイナリの先頭アドレス
  • _pngLen : pngバイナリのサイズ(byte数)
  • width : 画像幅を格納する変数
  • height : 画像高さを格納する変数
  • bpp : 何チャンネル持っているか。RGBなら3、RGBAなら4
  • 4 : どんな画像フォーマットも4チャンネル(RGBA)に変換するよう指定
  • stbi_load_from_memory() は、png画像を展開してベタに並んだピクセル情報のポインタを返す。

尚、利用し終わったら、確保したメモリを開放しないといけない。
  stbi_image_free(data);


Makefile は以下。

_Makefile (MinGW用)
_Makefile (MSYS2用)

# MinGW

03_loadpngwithstb.exe: 03_loadpngwithstb.c texture.h Makefile
    gcc 03_loadpngwithstb.c -o 03_loadpngwithstb.exe -D FREEGLUT_STATIC -lfreeglut_static -lopengl32 -lwinmm -lgdi32

texture.h: texture.png
    .\bin2c.exe -o texture.h texture.png

.PHONY: clean
clean:
    rm -f 03_loadpngwithstb.exe
    rm -f texture.h
    rm -f *.o

libpng を使った場合と大体同じだけれど、libpng と zlib(libz) を使わないので、-lpng -lz を省くことができた。

make と打ってビルド。03_loadpngwithstb.exe が得られた。実行すると以下のウインドウが表示された。

03_loadpngwithstb_ss.png


stbライブラリを使うことでも、exeファイル内のpngバイナリを読み込んで、OpenGL のテクスチャとして利用できることが分かった。

stbライブラリを使った場合、以下のメリットがあると言えそう。
  • 導入が簡単。
  • libpngを使った事例と比べると、圧倒的に少ない記述で済む。

逆にデメリットとしては、セキュリティホール面が不安、という点だろうか…。おそらく、libpngほど厳しくチェックされてるわけではないだろう…。そのあたりについては、配布ページでも言及されている。

This project discusses security-relevant bugs in public in Github Issues and Pull Requests, and it may take significant time for security fixes to be implemented or merged. If this poses an unreasonable risk to your project, do not use stb libraries.

nothings/stb: stb single-file public domain libraries for C/C++ より

「セキュリティホールの修正は遅れがちになるのでリスクを考えながら使ってね」てな感じだろうか。

もっとも今回、正体不明の画像を読み込ませるわけではないし…。自分で作成したpng画像を読み込むために使っているわけだから、特にデメリットは無さそうな気もする。

#2 [nitijyou] 弟が戻っていった

14:30頃に実家を出発して、17:00頃には帰宅したらしい。

2024/01/03(水) [n年前の日記]

#1 [prog] objcopyについて調べていた

先日、bin2c というツールを使えば C/C++で作ったexeの中にバイナリを含めることができる、と知ったのだけど。

_mieki256's diary - bin2cについて

以下のページで、objcopy というツールを使っても似たようなことができると知った。

_バイナリファイルの埋め込みかた
_メモ帳objcopyとobjdump
_Embedding a File in an Executable, aka Hello World, Version 5967 | Linux Journal
_objcopy(1) manページ
_ゼロからのOS自作入門 5章 osbook_day05c の objcopy のオプションメモ

objcopy は GNU Binutils に含まれているツールで、何かしらのバイナリファイルを、.o (オブジェクトファイル)に変換してくれる。.o になってしまえば、それをリンカに渡して結合することで、exe にそのバイナリを含めることができる。

bin2c的なやり方と比べた場合のメリットして、コンパイル時間を短くできる、ファイルサイズが大きくても結合できる可能性が高まる、等があるらしい。

気になったので試用してみた。環境は Windows10 x64 22H2 + MinGW (gcc 6.3.0) or MSYS2 MINGW64 (gcc 13.2.0)。

objcopyがインストールされてるか確認 :

MinGW上で、objcopy がインストールされているのか確認してみる。
> which objcopy
"D:\MinGW\bin\objcopy.exe"

> objcopy --version
GNU objcopy (GNU Binutils) 2.28
Copyright (C) 2017 Free Software Foundation, Inc.
This program is free software; you may redistribute it under the terms of
the GNU General Public License version 3 or (at your option) any later version.
This program has absolutely no warranty.

MinGW (SourceForge版) の場合、objcopy 2.28 が入ってた。


MSYS2 MINGW64上でも確認。
$ which objcopy
/mingw64/bin/objcopy

$ objcopy --version
GNU objcopy (GNU Binutils) 2.41
Copyright (C) 2023 Free Software Foundation, Inc.
...

MSYS2 MINGW64 には、objcopy 2.41 が入ってた。

使ってみる :

例えば、texture.png を texture.o にしてみたい。

Windows10 + MinGW (32bit, i386) 上で使うので…以下のような指定になるのだろうか。elf32-i386 とかそのあたりの指定は一体何なのか分からんけど…。
objcopy -I binary -O elf32-i386 -B i386 texture.png texture.o

たしかに texture.o が得られた。中身はどうなっているのだろう。objdump を使うと、.o の情報が得られるらしい。
> objdump -x texture.o

texture.o:     file format elf32-i386
texture.o
architecture: i386, flags 0x00000010:
HAS_SYMS
start address 0x00000000

Sections:
Idx Name          Size      VMA       LMA       File off  Algn
  0 .data         00019328  00000000  00000000  00000034  2**0
                  CONTENTS, ALLOC, LOAD, DATA
SYMBOL TABLE:
00000000 l    d  .data  00000000 .data
00000000 g       .data  00000000 _binary_texture_png_start
00019328 g       .data  00000000 _binary_texture_png_end
00019328 g       *ABS*  00000000 _binary_texture_png_size

-t をつければシンボルテーブルだけを確認できる。
> objdump -t texture.o

texture.o:     file format elf32-i386

SYMBOL TABLE:
00000000 l    d  .data  00000000 .data
00000000 g       .data  00000000 _binary_texture_png_start
00019328 g       .data  00000000 _binary_texture_png_end
00019328 g       *ABS*  00000000 _binary_texture_png_size

「SYMBOL TABLE:」に注目。3つのシンボルが定義されてる。
_binary_texture_png_start       // 開始位置を示す
_binary_texture_png_end         // 終了位置を示す
_binary_texture_png_size        // データサイズ

Cソースから利用する :

objcopy で変換した .o には3つのシンボルが設定されてるので、.c から利用する際はそのシンボルを使えばいい。例えば以下のような記述になる。

extern const unsigned char binary_texture_png_start[];  // start address
extern const unsigned char binary_texture_png_end[];    // end address
extern const unsigned char binary_texture_png_size[];   // size

// ...

  {
    const unsigned char *png_ptr = binary_texture_png_start;    // start address
    size_t png_size = (size_t)binary_texture_png_size;          // size

    texture = createTextureFromPngInMemory(png_ptr, png_size);
  }

ここでちょっとハマった。各シンボルの先頭の、「_」(underscore)を削除しつつ記述していることに注意。

Linux環境や、MSYS2 MINGW64 の場合は、各シンボルの先頭の「_」を削除しなくてもいいらしいけど…。Windows + MinGW の場合は、「_」を削除しないと 「undefined reference to `_binary_texture_png_start'」(「_binary_texture_png_start なんて定義されてねえよ」)といったエラーが出てしまう。おそらくは、Windows + MinGW だけの罠…。

このあたりについては、以下のやりとりで色々書かれている。

_c - Embedding binary blobs using gcc mingw - Stack Overflow

MinGW を使っているか、MSYS2 MINGW64 を使っているかで、記述を変えないといけないあたりは面倒だなと…。同じソースを利用できないではないか…。

一応、Cコンパイラ(プリプロセッサ)の定義済みマクロを使って、#ifdef - #else - #endif で記述を分けることはできそう。以下のような感じだろうか。
#ifdef __MINGW64__
// MSYS2 MINGW64
extern const unsigned char _binary_texture_png_start[];  // binary start address
extern const unsigned char _binary_texture_png_end[];    // binary end address
extern const unsigned char _binary_texture_png_size[];   // binary size
#else
// MinGW
extern const unsigned char binary_texture_png_start[];  // binary start address
extern const unsigned char binary_texture_png_end[];    // binary end address
extern const unsigned char binary_texture_png_size[];   // binary size
#endif

// ...

  {
#ifdef __MINGW64__
    // MSYS2 MINGW64
    const unsigned char *png_ptr = _binary_texture_png_start;    // start address
    size_t png_size = (size_t)_binary_texture_png_size;          // size
#else
    // MinGW
    const unsigned char *png_ptr = binary_texture_png_start;    // start address
    size_t png_size = (size_t)binary_texture_png_size;          // size
#endif

    texture = createTextureFromPngInMemory(png_ptr, png_size);
  }

このあたり、他に上手い方法は無いのかなあ…。

余談。各環境の定義済みマクロについては以下を参考にした。

_Qt (c++)でWindowsとLinuxを認識するマクロ - スタック・オーバーフロー
_C言語で定義済みマクロによるコンパイラ及びOS判定 [新石器Wiki]
_定義済みマクロ - おなかすいたWiki!
_Pre-defined Compiler Macros / Wiki / Compilers

objcopyの代わりにldを使う :

前述のページの中で、objcopy ではなく ld を使って、何かしらのバイナリファイルを .o にする方法があると知った。

_c - Embedding binary blobs using gcc mingw - Stack Overflow

例えば、texture.png を texture.o にするなら、以下のような感じだろうか。
ld -r -b binary -o texture.o texture.png
i386 がどうとか指定しなくて済むので、こちらのほうがいいかもしれない。

objdump で .o の情報を確認してみる。
$ objdump -x texture.o

texture.o:     file format pe-x86-64
texture.o
architecture: i386:x86-64, flags 0x00000038:
HAS_DEBUG, HAS_SYMS, HAS_LOCALS
start address 0x0000000000000000

Characteristics 0x5
        relocations stripped
        line numbers stripped

Time/Date               Thu Jan 01 09:00:00 1970
Magic                   0000
MajorLinkerVersion      0
MinorLinkerVersion      0
SizeOfCode              0000000000000000
SizeOfInitializedData   0000000000000000
SizeOfUninitializedData 0000000000000000
AddressOfEntryPoint     0000000000000000
BaseOfCode              0000000000000000
ImageBase               0000000000000000
SectionAlignment        00000000
FileAlignment           00000000
MajorOSystemVersion     0
MinorOSystemVersion     0
MajorImageVersion       0
MinorImageVersion       0
MajorSubsystemVersion   0
MinorSubsystemVersion   0
Win32Version            00000000
SizeOfImage             00000000
SizeOfHeaders           00000000
CheckSum                00000000
Subsystem               00000000        (unspecified)
DllCharacteristics      00000000
SizeOfStackReserve      0000000000000000
SizeOfStackCommit       0000000000000000
SizeOfHeapReserve       0000000000000000
SizeOfHeapCommit        0000000000000000
LoaderFlags             00000000
NumberOfRvaAndSizes     00000000

The Data Directory
Entry 0 0000000000000000 00000000 Export Directory [.edata (or where ever we found it)]
Entry 1 0000000000000000 00000000 Import Directory [parts of .idata]
Entry 2 0000000000000000 00000000 Resource Directory [.rsrc]
Entry 3 0000000000000000 00000000 Exception Directory [.pdata]
Entry 4 0000000000000000 00000000 Security Directory
Entry 5 0000000000000000 00000000 Base Relocation Directory [.reloc]
Entry 6 0000000000000000 00000000 Debug Directory
Entry 7 0000000000000000 00000000 Description Directory
Entry 8 0000000000000000 00000000 Special Directory
Entry 9 0000000000000000 00000000 Thread Storage Directory [.tls]
Entry a 0000000000000000 00000000 Load Configuration Directory
Entry b 0000000000000000 00000000 Bound Import Directory
Entry c 0000000000000000 00000000 Import Address Table Directory
Entry d 0000000000000000 00000000 Delay Import Directory
Entry e 0000000000000000 00000000 CLR Runtime Header
Entry f 0000000000000000 00000000 Reserved

Sections:
Idx Name          Size      VMA               LMA               File off  Algn
  0 .data         00019330  0000000000000000  0000000000000000  00000064  2**4
                  CONTENTS, ALLOC, LOAD, DATA
  1 .rdata        00000000  0000000000000000  0000000000000000  00000000  2**4
                  ALLOC, LOAD, READONLY, DATA
SYMBOL TABLE:
[  0](sec  1)(fl 0x00)(ty    0)(scl   2) (nx 0) 0x0000000000000000 _binary_texture_png_start
[  1](sec  1)(fl 0x00)(ty    0)(scl   2) (nx 0) 0x0000000000019328 _binary_texture_png_end
[  2](sec -1)(fl 0x00)(ty    0)(scl   2) (nx 0) 0x0000000000019328 _binary_texture_png_size

含まれる情報が一気に増えた気もするけど…。シンボルについては objcopy を利用した時と同様に3つ用意されてるので、利用に関しては問題無さそう。

OpenGLで描画するサンプル :

昨日実験したソレと同様に、メモリ上のpngバイナリをstbライブラリで処理してOpenGLのテクスチャとして利用できるのか試した。pngバイナリを exe に含める際、昨日は bin2c を使ったけれど、今回は objcopy もしくは ld を使ってみた。

_メモリ上のpngバイナリをOpenGLのテクスチャとして利用

C言語のソースは以下。ビルドには、freeglut, stb(stb_image.h) が必要。

_04_loadpng_objcopy.c
// OpenGL + stb sample. PNG image is in memory
// use objcopy. png file to object file

#include <stdio.h>
#include <stdlib.h>
#include <GL/freeglut.h>

// use stb library
#define STB_IMAGE_IMPLEMENTATION
#include "stb_image.h"

// png image file binary
#ifdef __MINGW64__
// MSYS2 MINGW64
extern const unsigned char _binary_texture_png_start[];  // binary start address
extern const unsigned char _binary_texture_png_end[];    // binary end address
extern const unsigned char _binary_texture_png_size[];   // binary size
#else
// MinGW
extern const unsigned char binary_texture_png_start[];  // binary start address
extern const unsigned char binary_texture_png_end[];    // binary end address
extern const unsigned char binary_texture_png_size[];   // binary size
#endif

GLuint texture;

/**
 * Load texture from png image on memory
 *
 * @param[in] _pngData png binary on memory
 * @param[in] _pngDataLen png binary size
 * return GLuint OpenGL texture ID. if 0, process fails
 */
GLuint createTextureFromPngInMemory(const unsigned char* _pngData, int _pngLen)
{
  GLuint texture;
  int width = 0, height = 0, bpp = 0;
  unsigned char *data = NULL;

  data = stbi_load_from_memory(_pngData, _pngLen, &width, &height, &bpp, 4);

  // create OpenGL texture
  glGenTextures(1, &texture);
  glBindTexture(GL_TEXTURE_2D, texture);
  glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA, width, height, 0, GL_RGBA, GL_UNSIGNED_BYTE, data);

  // Release allocated all memory
  stbi_image_free(data);

  return texture;
}

// draw OpenGL
void display()
{
  glClear(GL_COLOR_BUFFER_BIT);

  glColor4f(1.0f, 1.0f, 1.0f, 1.0f);

  // enable texture
  glEnable(GL_TEXTURE_2D);
  glBindTexture(GL_TEXTURE_2D, texture);

  // draw polygon
  glBegin(GL_QUADS);

  GLfloat v = 0.85f;

  glTexCoord2f(0.0, 0.0); // set texture u, v
  glVertex2f(-v, v);

  glTexCoord2f(0.0, 1.0);
  glVertex2f(-v, -v);

  glTexCoord2f(1.0, 1.0);
  glVertex2f(v, -v);

  glTexCoord2f(1.0, 0.0);
  glVertex2f(v, v);

  glEnd();

  // disable texture
  glDisable(GL_TEXTURE_2D);

  glFlush();
}

// Keyboard callback function
void keyboard(unsigned char key, int x, int y)
{
  switch (key)
    {
      case '\x1B':
      case 'q':
        // Exit on escape or 'q' key press
        glutLeaveMainLoop();
        // exit(EXIT_SUCCESS);
        break;
    }
}

int main(int argc, char **argv)
{
  glutInit(&argc, argv);

  glutInitDisplayMode(GLUT_RGBA);
  glutInitWindowSize(1280, 720);
  glutCreateWindow("OpenGL Texture Example");

  // create OpenGL texture from PNG image
  {
#ifdef __MINGW64__
    // MSYS2 MINGW64
    const unsigned char *png_ptr = _binary_texture_png_start;    // start address
    size_t png_size = (size_t)_binary_texture_png_size;          // size
#else
    // MinGW
    const unsigned char *png_ptr = binary_texture_png_start;    // start address
    size_t png_size = (size_t)binary_texture_png_size;          // size
#endif

    texture = createTextureFromPngInMemory(png_ptr, png_size);

    if (!texture)
      {
        fprintf(stderr, "Failed create texture\n");
        exit(1);
      }
  }

  glClearColor(0.2f, 0.4f, 0.8f, 1.0f);

  glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR);
  glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);

  glutKeyboardFunc(keyboard);
  glutDisplayFunc(display);

  glEnable(GL_BLEND);
  glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA);

  glutSetOption(GLUT_ACTION_ON_WINDOW_CLOSE, GLUT_ACTION_GLUTMAINLOOP_RETURNS);

  glutMainLoop();

  return EXIT_SUCCESS;
}

MinGW用の Makefile は以下。.exe を生成する際、texture.o を追加した。texture.o は objcopy で生成している。

_Makefile
# MinGW

04_loadpng_objcopy.exe: 04_loadpng_objcopy.c texture.o Makefile
    gcc 04_loadpng_objcopy.c texture.o -o 04_loadpng_objcopy.exe -D FREEGLUT_STATIC -lfreeglut_static -lopengl32 -lwinmm -lgdi32

texture.o: texture.png Makefile
    objcopy -I binary -O elf32-i386 -B i386 texture.png texture.o

#texture.o: texture.png Makefile
#   ld -r -b binary -o texture.o texture.png

.PHONY: clean
clean:
    rm -f 04_loadpng_objcopy.exe
    rm -f *.o


MSYS2 MINGW64用の Makefile は以下。texture.o は ld で生成している。

_Makefile.msys2
# MSYS2

04_loadpng_objcopy.exe: 04_loadpng_objcopy.c texture.o Makefile
    gcc 04_loadpng_objcopy.c texture.o -o 04_loadpng_objcopy.exe -D FREEGLUT_STATIC -lfreeglut -lopengl32 -lwinmm -lgdi32 -static

texture.o: texture.png Makefile
    ld -r -b binary -o texture.o texture.png

.PHONY: clean
clean:
    rm -f 04_loadpng_objcopy.exe
    rm -f *.o

make と打てば 04_loadpng_objcopy.exe をビルドできる。実行するとウインドウが開く。

04_loadpng_objcopy_ss.png


これで、obcopy、もしくは ld を使っても、png画像のバイナリを exe に含めることができると分かった。

#2 [prog] Makefileの中で処理を分けたい

ここ数日、MinGW用と、MSYS2 MINGW64用の、2つのMakefileを書きながら実験してるけど、なんだか面倒臭くなってきた。make -f Makefile.msys2 と打つべき場面で make と打ってしまってエラーが出てきたり…。ちょっと面倒臭い。

1つの Makefile の中で、MinGW上で使おうとしているのか、MSYS2 MINGW64上で使おうとしているのか判別して、処理を分けることはできないのだろうか。

使ってるのは GNU Make。3.81 と 4.4.1。

処理を分けることもできるらしい :

調べてみたら、Makefile の中でも、ifeq - else - endif や ifdef - else - endif といった書き方ができるらしい。

また、MSYS2 上で作業する場合、環境変数 MSYSTEM に、MINGW32 とか MINGW64 といった文字列が入るらしい。
$ echo $MSYSTEM
MINGW64

MinGW上では、何も入らない。
> echo %MSYSTEM%
%MSYSTEM%

であれば、こういう書き方ができるかな…。
ifeq ($(MSYSTEM),MINGW64)

# MSYS2 MINGW64

04_loadpng_objcopy.exe: 04_loadpng_objcopy.c texture.o Makefile
    gcc 04_loadpng_objcopy.c texture.o -o 04_loadpng_objcopy.exe -D FREEGLUT_STATIC -lfreeglut -lopengl32 -lwinmm -lgdi32 -static

texture.o: texture.png Makefile
    ld -r -b binary -o texture.o texture.png

else

# MinGW

04_loadpng_objcopy.exe: 04_loadpng_objcopy.c texture.o Makefile
    gcc 04_loadpng_objcopy.c texture.o -o 04_loadpng_objcopy.exe -D FREEGLUT_STATIC -lfreeglut_static -lopengl32 -lwinmm -lgdi32

texture.o: texture.png Makefile
    objcopy -I binary -O elf32-i386 -B i386 texture.png texture.o

#texture.o: texture.png Makefile
#   ld -r -b binary -o texture.o texture.png

endif

.PHONY: clean
clean:
    rm -f 04_loadpng_objcopy.exe
    rm -f *.o

サンプルについてくる Makefile としては、ちょっと分かりづらくなるな…。でもまあ、フツーはコンパイルの仕方を書かないでソースだけ紹介するサンプルが大半だし。まだ Makefile があるだけマシかも。

余談。特殊変数について :

余談。本来 Makefile を書くときは、$@ や $< 等の変数を使うのが当たり前だけど。

_Makefile の特殊変数・自動変数の一覧 | 晴耕雨読
_トリビアなmakefile入門

そのあたりを書いちゃうと、一見どういうことをしてるのか分からなくなりそうなので、あえてベタ書きしてます。ただ、こういう書き方をしてしまうなら、いっそのことbatファイルでもええやん、とも…。

2024/01/04(木) [n年前の日記]

#1 [prog] xxdというコマンドがあることを知った

先日、bin2c を使えば、バイナリファイルをC言語のヘッダーファイル(unsigned char配列)に変換できる ―― 例えばpngバイナリをC言語のヘッダーファイルにしてexeファイルの中に含めることができる、と分かったけれど。

その後ググっていたら、xxdなるツール? コマンド? があることを知った。この xxd を使っても、バイナリファイルをC言語のヘッダーファイルに変換できるらしい。

_【xxd】コマンド――ファイルを16進数でダンプする、ダンプから復元する:Linux基本コマンドTips(254) - @IT
_A-Liaison BLOG: xxd を使って画像などのバイナリデータをソースコードに含める方法

元々は vim に付属するツールだそうで、vim が入ってる環境なら xxd も一緒に入ってる可能性が高そうだなと…。

試しに使ってみた。環境は Windows10 x64 22H2 + MinGW(gcc 6.3.0) or MSYS2(gcc 13.2.0)。

xxdがある場所 :

MinGW(SourceForge版、gcc 6.3.0) には入っていた。msys 1.0 側に入っている。
> which xxd
"D:\MinGW\msys\1.0\bin\xxd.exe"

> xxd --version
xxd V1.10 27oct98 by Juergen Weigert

MSYS2 MINGW64(gcc 13.2.0) にも入っている。
$ which xxd
/usr/bin/xxd

$ xxd --version
xxd 2023-10-25 by Juergen Weigert et al.

vim に付属するツール、という話があったけど、MSYS2 の vim のパッケージ情報を眺めたら、たしかに一緒に入っていた。もし xxd が見つからなかったら、vim をインストールすると xxd も入るかもしれない。

_Package: vim - MSYS2 Packages


ちなみに、Ubuntu Linux のパッケージを調べたら、xxd というパッケージが独立して存在していた。sudo apt install xxd でインストールできそう。

C言語のヘッダーファイルとして出力 :

xxd に -i オプションをつけることで、C言語のヘッダーファイルの形で出力できるらしい。そのままだと標準出力に出力されるので、「>」を使ってファイルに書き込む。

xxd -i texture.png > texture.h

出力された内容は以下のような感じ。バイナリデータと、バイナリデータのサイズが記述されている。
unsigned char texture_png[] =
{
  0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a, 0x00, 0x00, 0x00, 0x0d,
  0x49, 0x48, 0x44, 0x52, 0x00, 0x00, 0x02, 0x00, 0x00, 0x00, 0x02, 0x00,
// ...
  0x10, 0x01, 0xd1, 0x1a, 0x6f, 0xdc, 0xef, 0xe1, 0x00, 0x00, 0x00, 0x00,
  0x49, 0x45, 0x4e, 0x44, 0xae, 0x42, 0x60, 0x82
};
unsigned int texture_png_len = 103208;

先日、bin2c を使って実験していたアレコレを、xxd を使って処理できるのか試してみたけど、問題なく動いてくれた。

bin2c は別途入手しないと使えないけれど、xxd ならあらかじめ入ってる環境が多そうなので、こちらを使ったほうが良さそうだなと…。

#2 [android][digital] Android版Google Chromeでhttpがhttpsにされてしまう

Androidスマートフォン、HUAWEI P9 lite (Android 7.0) + Chrome 119.0.6045.193 で、URLが http から始まるサイトにアクセスしようとしたところ、URLが https に勝手に書き換えられて困ってしまった。

表示されないサイトは以下。

_OpenGL入門

URL が、 _http://wisdom.sakura.ne.jp/system/opengl/ なのだけど。Android版 Chrome 上では以下のページで紹介されている表示内容になってしまう。

_公開中のWebサイトにアクセスできなくなった(主にGoogle Chrome、Microsoft Edge) - よくあるご質問 - さくらのサポート情報

PC版 Google Chrome 120.0.6099.200 x64 なら、アドレス欄(アドレスバー)で https を http に書き換えることでアクセスできるのだけど…。Android版 Chrome 119.0.6045.193 では、何度書き換えて試しても、https に書き戻される…。

Chrome側の設定変更でどうにかならんかと、以下の作業をしてみたけど変化無し。
仕方ないので、Android版 Firefox をインストールした。こちらなら問題無く開ける。

それにしても、Chrome はどうしてこんな状態になるんだろう…。そういえば、たしか Android 7.0上では、Chrome は 120.* に更新されないという話を見かけたっけ…。

_次期Google ChromeでAndroid 7のサポートが終了 - PC Watch

もしかしてこのあたり、Chrome 119.* でエンバグしたまま 120.* に移行しちゃって、119.* は放置されてしまったのでは…。それとも、実は Android 7.0 ユーザに対する意図的な嫌がらせ…?

2024/01/05追記 :

Chromeの設定でなんとかなったかもしれない。
  • 設定 → プライバシーとセキュリティ → 常に安全な接続を使用する、をOFFに。
  • chrome://flags を開いて https-upgrades を Disabled に。
これで件のサイトが ―― _http://wisdom.sakura.ne.jp/system/opengl/ が開けるようになった。

しかし、全体的な設定を変えないと改善できないあたり、ちょっとどうなんだろうという気もする。こういう事例に遭遇した時だけ、ユーザの操作で一時的に http に切り替えられる機能がついてたら助かったのでは…。ユーザの意思と無関係に勝手に書き換えられるのは困る…。

まあ、嫌なら Firefox を使え、ということかな…。そもそも Chrome はもう更新されないし、極力 Firefox を使ったほうがマシなんだろう…。しかし Firefox では Googleのサービスが色々動かないのだよな…。

2024/01/05(金) [n年前の日記]

#1 [linux] MobaXtermでSFTP接続ができなくて悩んだ

Windows10 x64 22H2 + MobaXterm 23.6 build 5186 Personal Edition で、Ubuntu Linux 22.04 LTS や Ubuntu Linux 20.04 LTS をインストールしてあるサブPCに ssh でログインしようとしたのだけど、MobaXterm のウインドウの左側にファイル一覧が表示されなくて悩んだ。以前使っていた頃は表示されていた記憶があるのだけど…。

おそらく、ファイル一覧の取得、ファイルのアップロード/ダウンロードは、SFTP もしくは scp を使って実現しているのかなと思うのだけど…。

まずは ping で状態を確認。Windows10上から以下を打った。
ping hoge -4
ping hoge -6
-4 をつけると IPv4 で、-6 をつけると IPv6 でIPアドレスが分かる。一応、Linux機のIPv4アドレス、IPv6アドレスは得られた。

WinSCP 6.1.2 Portable を導入して、SFTPで接続できるか確認してみたけれど、問題なく接続できた。となると、MobaXterm の何かがおかしいということかな…。

MobaXterm の Session には SFTP接続も用意されているので、それを試してみた。エラーが出た。
Error EEISocketError : Socket handle is IPv6 and could not be used with IPv4 address
IPv4, IPv6 が関係してるらしい…?

WinSCP のログを確認したら、IPv4 で接続していた。つまり、MobaXterm が IPv6 ではなく IPv4 で接続していれば SFTP も使える可能性がある…?

MobaXterm の SFTP接続のホスト名を、ping で得られた IPv4アドレスに変更して試してみた。これだと繋がる…。やはり、IPv4 で接続しないと SFTP は使えないらしい。

であれば、MobaXterm 側でIPv6を無効にできる設定があれば解決しそう。しかし、そういった設定は見当たらなかった。どこかにあるのかもしれんけど、ざっと眺めた感じでは見つからず…。ググってみてもこのあたりに言及してるページが無い…。

でもまあ、ファイル転送をしたかったら WinSCP を使えばいいか…。それに、各 Linux機は samba をインストールして共有フォルダが使えるようにしてあるので、Windows + エクスプローラからLinux機の共有フォルダにアクセスできるし…。

2024/01/06() [n年前の日記]

#1 [windows][linux] Windows10でIPv4とIPv6の優先度を変更

Windows10 x64 22H2 + MobaXterm 23.6 build 5186 Personal Edition で、SFTP接続ができない症状で悩んでしまったけれど、ホスト名に相手先のIPv4アドレスを指定すればSFTP接続ができる、というところまでは分かった。

ということは、Windows10側で、IPv6 より IPv4 のほうが優先して使われるように設定すれば、ホスト名にPC名を入力してもSFTP接続もできるようになるのではないかと閃いた。

ググってみたら、IPv4 と IPv6 の優先度を変更する方法はあるらしい。

_Windows 10でIPv4とIPv6の優先度を変更する方法 - 4thsight.xyz
_Windowsで、IPv6 より IPv4 を優先に設定 - chinaz.org
_「IPv6無効設定」はもう古い?IPv6を有効にしたままIPv4の優先順位を上げる設定 - ぼくんちのTV 別館

一応そのあたり試してみた。

状態の確認 :

ping localhost と打った際の結果で状態が分かる。
> ping localhost

MAINPCNAME [::1]に ping を送信しています 32 バイトのデータ:
::1 からの応答: 時間 <1ms
::1 からの応答: 時間 <1ms
::1 からの応答: 時間 <1ms
::1 からの応答: 時間 <1ms

::1 の ping 統計:
    パケット数: 送信 = 4、受信 = 4、損失 = 0 (0% の損失)、
ラウンド トリップの概算時間 (ミリ秒):
    最小 = 0ms、最大 = 0ms、平均 = 0ms

「::1」と表示されたら、IPv6 が優先される状態になっている。

管理者権限で cmd.exe を開いて状態を確認。
netsh interface ipv6 show prefixpolicies
> netsh interface ipv6 show prefixpolicies

アクティブ状態を照会しています...

優先順位   ラベル  プレフィックス
----------  -----  --------------------------------
        50      0  ::1/128
        40      1  ::/0
        35      4  ::ffff:0:0/96
        30      2  2002::/16
         5      5  2001::/32
         3     13  fc00::/7
         1     11  fec0::/10
         1     12  3ffe::/16
         1      3  ::/96
この並びになってる場合は、IPv6 が優先して使われる状態になっている。らしい。

優先度を変更 :

「::ffff:0:0/96」が一番上になれば、IPv4 が優先された状態になるとのこと。以下を打ち込めば設定を変更できる。
netsh interface ipv6 set prefixpolicy ::ffff:0:0/96 60 4
> netsh interface ipv6 set prefixpolicy ::ffff:0:0/96 60 4
 OK

状態を確認してみる。「::ffff:0:0/96」が一番上になってればヨシ。
> netsh interface ipv6 show prefixpolicies
アクティブ状態を照会しています...

優先順位   ラベル  プレフィックス
----------  -----  --------------------------------
        60      4  ::ffff:0:0/96
        50      0  ::1/128
        40      1  ::/0
        30      2  2002::/16
         5      5  2001::/32
         3     13  fc00::/7
         1     11  fec0::/10
         1     12  3ffe::/16
         1      3  ::/96

ping localhost の結果も変わる。
> ping localhost

MAINPCNAME [127.0.0.1]に ping を送信しています 32 バイトのデータ:
127.0.0.1 からの応答: バイト数 =32 時間 <1ms TTL=128
127.0.0.1 からの応答: バイト数 =32 時間 <1ms TTL=128
127.0.0.1 からの応答: バイト数 =32 時間 <1ms TTL=128
127.0.0.1 からの応答: バイト数 =32 時間 <1ms TTL=128

127.0.0.1 の ping 統計:
    パケット数: 送信 = 4、受信 = 4、損失 = 0 (0% の損失)、
ラウンド トリップの概算時間 (ミリ秒):
    最小 = 0ms、最大 = 0ms、平均 = 0ms

「127.0.0.1」が出てきたら、IPv4の優先度が高くなってる。

MobaXtermでの結果 :

IPv4 の優先度を高くしてみたけれど、MobaXterm は相変わらず SFTP接続できなかった…。効果無し…。

設定を元に戻そう…。

状態のリセット :

以下を打てば状態をリセットできる。
netsh interface ipv6 reset
上記の設定後、OSの再起動が必要。

2024/01/07() [n年前の日記]

#1 [python] PyOpenGLを再勉強中

C言語 + OpenGL でプログラムを書こうと思ったけれど、コンパイルして動作確認するのがちょっと面倒な気がしてきた。一旦仮で、Python + PyOpenGL を使ったプログラムを書いて、ある程度動きがまともになったところで C言語に移植してみることにした。

そんなわけで、PyOpenGL を再勉強中。書き方をすっかり忘れてる…。

#2 [digital] HDDレコーダからNASに番組をダビングできない

愚痴です。

HDDレコーダ TOSHIBA RD-BZ710 から、DTCP-IP対応NAS BUFFALO LS210D0201G に、録画番組(DRモードで録画、複数ファイルを結合)をダビング(コピー)しようとしたのだけど、かなり処理が進んだところで「中断されました」と表示されてしまって途方に暮れた。いやまあ、ムーブを選んで、万が一転送に失敗してファイルが消えたら痛いよなとコピーを選んでおいたから、一応元ファイルはまだ残ってるけど。こうして事故が起きて処理に失敗してるのに、コピー可能な回数だけはどうせきっちりと減っているのだろうな…。

確実に転送成功する保証もないのにコピー回数制限をつけるとか、このあたり、なんだかなあ…。いやまあ、TV放送されたコンテンツは一切の録画すら本来は許したくないと権利元は思っていて、故にここまで不自由な状態になっているのだろうけど…。それにしたって、ムーブに成功したらその時点でファイルを削除するとか、コピーに成功した時点でコピー回数を減らすとか、そういう仕様で作れるはずだろう…。

大体にして、ムーブに失敗したら取り返しがつかないから一応コピーにしておくか、などとユーザを不安にさせて、そんなことをわざわざやらせてる時点でクソ過ぎる…。

2024/01/08追記 :

改めて確認してみたら、さすがにコピー処理に失敗したときはコピー可能回数は減ってないように見えた。助かった。ただ、絶妙なタイミングで処理に失敗するとやっぱり減るんじゃないのかなという不安は拭えないけれど。

その後試していたら、特定のファイルが何度やってもコピーに失敗して、また途方に暮れた。ファイルが壊れてるんだろうか…? まさかとは思うけど、解像度が異なるファイルを結合すると必ずダビング(ムーブ or コピー)に失敗するファイルになってしまう、みたいな制限があったりしないだろうな…。複数ファイルを結合しちゃったから、もう分離できないのだけど…。

#3 [anime] 「サイボーグ009VSデビルマン」を視聴

BS12で放送されていたので視聴。全3話で構成されるアニメ作品。1話と2話は30分だけど、3話だけ尺が長いようで、1話+2話、3話で、2日に分けて放送されてた。

恥ずかしながら、こんな作品があったことを知らなかった…。作画はかなり頑張っていた印象。脚本は…まあ、VSモノってこんな感じですよね、と…。この2つを混ぜるのは大変だっただろうなあ…。

009もデビルマンも同じ世界に存在しているという設定だった。この後それぞれ、何もかも全く違う世界線になっていくはずだけど、両方存在していてもそんなにおかしくない時期というのがこのあたり、ということなのかなと。

009を福山潤さんが演じていて、これは全然アリだなと。改めて調べてみると、009は色んな方が演じているのね…。

_サイボーグ009の登場人物 - Wikipedia

2024/01/08(月) [n年前の日記]

#1 [python] PyOpenGLを再勉強中その2

PyOpenGLについて再勉強中。「宇宙飛行」スクリーンセーバ(ssstars.scr)と似た感じの描画処理をするサンプルプログラムを書いているところ。png画像を読み込んで、OpenGLのテクスチャにして描画するサンプル、程度にはなるのではないかと思いながら作業中。

宇宙飛行スクリーンセーバについては、以下でメモしてあった。

_mieki256's diary - hsp3dishの勉強を再開

#2 [digital] HDDレコーダからダビングするコツが少し分かってきた

HDDレコーダ TOSHIBA RD-BZ710 から、DTCP-IP対応NAS BUFFALO LS210D0201G に録画番組をダビング(コピー or ムーブ)できなくて悩んでいたけれど、なんとなくコツが分かってきた。まあ、この機種だけのコツだろうけど。

まず、ダビングできない番組(ファイル)は、何度ダビングを試しても必ず同じところで「処理がキャンセルされました」と言ってくる。何度か試していればそのうち何かの拍子に上手く行くのではないかと淡い期待を持って再チャレンジしても完全に無駄だと分かった。

ダビングできない番組の中には、何かしら異常な部分があるのだろう。まずはその異常部分を特定しないと…。おそらくだけど、ファイルを結合した時に異常部分ができてしまう気配がする…。結合部分で妙なことになっているのではないか…。たぶん。おそらく。

複数ファイルを結合してしまったファイルしか残ってない場合、RD-BZ710 では「プレイリスト」を利用して、異常ではない部分だけを抽出しつつダビングすることができる。編集モード、かつ、チャプター表示モードにして、プレイリスト編集を選び、ダビングできそうなチャプターだけを選んでプレイリストに追加。そのプレイリストを「ダビング」することで、NASにムーブすることができた。尚、「コピー」「ムーブ」のうち、「ムーブ」を選べば、ムーブ後、元ファイルの中からムーブしたチャプターだけが削除される。元ファイルの全てを丸々全部削除するわけではないあたりは助かった…。

転送処理に失敗した場合、コピー回数は減らないし、ムーブに失敗した場合も元ファイルは残ってくれることが分かった。まあ、失敗するタイミングによっては消されてしまう可能性も否定できないけれど…。ほとんどの場合は、転送失敗時に元ファイルが消滅することはなさそう。いやまあ、運が良ければ、という話かもしれないけれど。

2024/01/09(火) [n年前の日記]

#1 [python] PyOpenGLで宇宙飛行モドキを書いた

Windows10 x64 22H2 + Python 3.10.10 64bit + PyOpenGL 3.1.6 で、宇宙飛行スクリーンセーバっぽい見た目のサンプルを書いてみた。

ソースが長くなってしまったので、Gist に置いておきます。

_01_ssstar_opengl.py

一応バックアップとして、ここにも置いておきます。

_01_ssstar_opengl.py
_star.png

動かすと以下のような感じ。




これをC言語に移植してみる予定。

#2 [anime] 「プロメア」を視聴

正月頃にBS11で放送されたソレを録画していたので視聴。アニメ映画。「グレンラガン」「キルラキル」の今石洋之監督+中島かずき脚本の作品。制作はトリガー。3DCGはサンジゲン。

映像凄いな…。三角形と四角形を意識しつつ、かつ、フラットというかソリッドな感じの絵柄が実にクール。なぜか「スターブレード」「MEGA-CD版シルフィード」を連想してしまった。あの頃のソリッドな映像スタイルって、アレはアレでイイ感じだよなと…。アクションも派手派手。金田伊功フォロワーの監督さんの作品だけあって、バッキバキな動きが目白押し。

ただ、何をしているのか分からないカットだらけだなと…。どうしてこんなに見づらくなるのか…。今石監督は、日本のアニメのセル部分と背景部分のスタイルの違いが気になっていて統一できないか過去作品でもチャレンジしてたらしいけど、そのあたりが関係して…。いや、そこだけではないような気がする…。手描きアニメは目で見てイイ感じに線を省略して描けるけど、3DCGは線を省略できない、といったあたりが関係していたりしないか…。 *1

主人公を演じてた方の演技がちょっと…。これはどうなんだ…。実写畑では有名な役者さんだし、フツーのシーンは全く問題ないのだけど、叫ぶ演技が…。まあそのあたりは声優さんでもなかなか難しいという話をどこかで見かけたような記憶もあるし、こうなるのも仕方ないのかなあ…。フツーの役者さんって延々叫び続ける演技を要求されたりしないし…。

後半、めっちゃグレンラガンな印象を受けた。もしかするとグレンラガン Ver.2.0 に相当する作品なのかな…。だとしたらぜひとも Ver.3.0 を作ってほしいところ。家は3回建てると満足できる家になると言うし、Microsoft製品はVer.3.0から使い物になるという話もあるので、映画の類もVer.3.0は名作になる可能性が高いのではないか、と思うわけで。実際、細田守監督が「ぼくらのウォーゲーム」Ver.3.0で海外から高評価を受けてたし。「なんだよまたこのネタかよ」と言い出す人も居るのだろうけどそんなの一切気にせずにVer.3.0を作るのもアリだと思います。大体にして同じネタ云々を気にしてたら宮崎アニメなんてどれも見れなくなるし。
*1: それとも単に自分が歳をとって脳が老化したせいだろうか…。フレームとフレームの間に実在してないはずのフレームを脳内で作り出す能力が落ちてる…?

2024/01/10(水) [n年前の日記]

#1 [windows] foobar2000+MIDI Playerでハマった

Windows10 x64 22H2上に、音楽再生アプリ foobar2000 2.1 x86 をインストールした。

_foobar2000

せっかくだから、MIDIファイルを再生できるコンポーネント、MIDI Player 2.9.2.0 もインストールしたのだけど、音がショボい…。とんでもなくショボい。

_foobar2000: Components Repository - MIDI Player
_foobar2000: Components Repository

せめて、SoundFontを使えるようにしたい。

MIDI Player 2.9.0.0 の時点で fluidsynth を再追加したと書いてあった。fluidsynth をインストールしてやれば、SoundFont が使えるようになるのかもしれない。

_Releases - FluidSynth/fluidsynth

foobar2000 は 32bit版(x86) をインストールしてあるので、fluidsynth も32bit版を選ぶ。fluidsynth-2.3.4-winXP-x86.zip を入手して解凍。任意の場所に置く。今回は、D:\Prog\fluidsynth\fluidsynth-2.3.4-winXP-x86\ に置いた。

PATHが通ってないと、foobar2000 + MIDI Player から選べない、と書いてあった。環境変数 PATH を修正して、fluidsynthインストールフォルダ\bin にPATHを通しておいた。この状態なら、MIDI Player の設定画面で項目が出てくる。

しかし、configureボタンが押せない。SoundFont (.sf2) を指定できない…。

fluidsynth.exe をDOS窓(cmd.exe上)で実行したら、C:\ProgramData\soundfonts\default.sf2 が無いと言ってきた。
> fluidsynth.exe
FluidSynth runtime version 2.3.4
Copyright (C) 2000-2023 Peter Hanappe and others.
Distributed under the LGPL license.
SoundFont(R) is a registered trademark of Creative Technology Ltd.

fluidsynth: error: fluid_sfloader_load(): Failed to open 'C:\ProgramData\soundfonts\default.sf2': File does not exist.
fluidsynth: error: Unable to open file 'C:\ProgramData\soundfonts\default.sf2'
fluidsynth: error: Failed to load SoundFont "C:\ProgramData\soundfonts\default.sf2"
fluidsynth: error: not enough MIDI in devices found. Expected:1 found:0
fluidsynth: error: Device "default" does not exists
Failed to create the MIDI thread; no MIDI input
will be available. You can access the synthesizer
through the console.
Type 'help' for help topics.
とりあえず、その場所に、そういうファイル名のSoundFontファイルを置いておけばいいのだろうか?

SGM-V2.01.sf2 を発掘して、該当場所にコピー。default.sf2 にリネーム。

この状態なら、foobar2000 + MIDI Player 2.9.2.0 でも再生できた。

しかし、SoundFont を変えてみたいと思った時に、一々こんなことしてられない。古い版なら SoundFont を指定できた記憶があるのだけどなあ…。

_Releases - stuerp/foo_midi

幸い、github上でまだ古い版がいくつか公開されていた。MIDI Player 2.7.4.4 (foo_midi.fb2k-component) を入手してそちらをインストールしてみた。これなら foobar2000 の設定画面から SoundFont を変更できる。古いからバグがあるのかもしれんけど、この版を使うことにしよう…。

BASSMIDIについて :

MIDI Player は BASSMIDIとやらも使えるらしいのだけど、ググって探して bassmididrv.exe を見つけてインストールしてみたものの無反応。Configure BASSMIDI Driver を実行して、SoundFontも指定したはずなのだけど…。

余談。コンポーネントのインストール方法 :

拡張子が .fb2k-component のファイルを入手。実態は .zip ファイルらしい。foobar2000 が起動してる状態で Ctrl + P を押すと設定画面が開く。その中の Components を選んで、.fb2k-component ファイルをドラッグアンドドロップしてやる。その後「OK」ボタンをクリックすれば foobar2000 が再起動して、管理者権限で何かの処理をしていいかと尋ねてくる。

2024/01/11(木) [n年前の日記]

#1 [prog] C言語で宇宙飛行スクリーンセーバっぽい描画をするプログラムを書いてみた

_先日、 Python + PyOpenGL を使って、宇宙飛行スクリーンセーバ(ssstars.scr)っぽい描画をするスクリプトを書いたのだけど。ソレを、C言語 + OpenGL (+ freeglut) 用に移植してみた。

環境は、Windows10 x64 22H2 + MinGW (gcc 6.3.0) or MSYS2 MINGW64 (gcc 13.2.0) + freeglut 3.0.0 or 3.4.0-2。

ソースその他 :

ソースが長くなってしまったので、gist に置いておく。

_Like ssstars screensaver for C and OpenGL

一応ココにも置いておくけど…。

_01_ssstars_opengl.c
_Makefile.txt
_texture.h
_texture.png

stbライブラリ(stb_image.h) を使ってpngを読み込んでいるので、ビルドには stb_image.h も必要。入手して、.c と同じ階層に置く。

_stb/stb_image.h at master - nothings/stb
_nothings/stb: stb single-file public domain libraries for C/C++

ビルド :

Makefile も用意したので、MinGW (gcc 6.3.0) が使える状態で make と打てば、01_ssstars_opengl.exe が生成される。

実行結果 :

01_ssstars_opengl.exe を実行すると以下のような感じの見た目になる。

今回のポイント :

今回書いたプログラムのポイントは…。
  • exeファイルの中にpng画像を含めてみた。これで、画像を描画するプログラムも、exeファイル1つを配布するだけで済む。別途、画像ファイルまで配布しなくて良い。
  • メモリ上(exeファイル内)のpng画像バイナリを読み込んで OpenGLのテクスチャとして利用している。

exeファイルにpng画像を含めるあたりは、xxd を使って、バイナリファイルをC言語のヘッダファイルの状態に変換して、#include で .c に内包してしまうことで実現してる。

_xxdというコマンドがあることを知った - mieki256's diary

安直かもしれないけれど、確実と言えば確実かなと…。ただ、バイナリのファイルサイズが大き過ぎる場合は問題が出てくるかもしれない。その場合は objcopy を使うことになるのかも。

_objcopyについて調べていた - mieki256's diary

メモリ上のpng画像バイナリを読み込むあたりは、stbライブラリ(stb_image.h)を使用。

#2 [windows] WindowsUpdateが0x80070643エラーを出したので回復パーティションサイズを増やしてみた

Windows10 x64 22H2上で、WindowsUpdate が 0x80070643 エラーを延々出し続ける状態になってしまった。

ググってみたところ、回復パーティションのサイズが足りないと、件のエラーが出るらしい…。なんじゃそりゃ…。

_Windows Update KB5034441 0x80070643 エラー - FF11どらプリを心待ちにしている、わしの倉庫
_KB5034441が0x80070643エラーで失敗するとの不具合報告が多数。Windows10にて | ニッチなPCゲーマーの環境構築Z

なんともふざけた話だなと。と言うのも、自分、去年の年末、Windows10を再インストールしていて。その際、各パーティションサイズについては、Windows10のセットアップディスクに任せて、自動でサイズを切らせていたわけで…。

しかし、デフォルトでは回復パーティションのサイズが500MB+αぐらいしかなくて、「オイオイ。そのサイズで大丈夫か?」「たしか以前、回復パーティションのサイズが足りなくて不具合を起こしていた話を目にしたぞ?」と不安になったものの、「最新のセットアップディスクが『このサイズで十分ですよ』と処理しているのだから、まあこれでも大丈夫なのかなあ」とそのままにしてしまったわけで…。

やっぱりダメじゃねえか! ふざけんな! 「このサイズで十分ですよ」ってお前コレ全然足りてないやんけ! 案の定こんなトラブル起こしやがって! ムキー!

デフォルト状態でインストールするとこうして必ず不具合を起こすことになる、この状況はマズいよなあ。どうして最初から2GBぐらい確保するように修正しておかないのかなあ。一体何をやってるんだ、Microsoft。

それはともかく。このまま放置していてもずっとエラーを出し続けて、他の WindowsUpdate が処理されない状態に陥りそうな気がする…。回復パーティションサイズを増やす方法については、公式に記事が公開されてるらしいので、一応作業してみることにした。

手順をメモ :

作業手順は、以下の英文記事で紹介されているらしい。

_KB5028997: Instructions to manually resize your partition to install the WinRE update - Microsoft Support

DeepLで翻訳しながら作業を進めていった。

何をやっているのかをざっくり説明すると、以下のようなことをしているようだなと…。
  1. WinRE(回復パーティション)を無効にする。
  2. OSが入っているパーティション(Cドライブ)のサイズを少し縮小する。
  3. 回復パーティションを削除。
  4. 回復パーティションを再作成。この時、Cドライブを縮小した分、回復パーティションのサイズが増える。
  5. 回復パーティションをフォーマット。
  6. WinREを有効化。

注意点その1。上記の流れを見れば分かるように、Cドライブのすぐ後ろに回復パーティションがあることを前提にして作業している。もし、そういうパーティション構成になってなかったら、この作業はできない。まあ、回復パーティションの直前に、サイズを縮小できそうなパーティションがあれば、どうにか作業できるだろうけど…。

注意点その2。Cドライブは空き容量がそれなりにあること。空き容量がほとんど残ってなかったり、パーティションの後ろのほうに移動できないファイル等が存在しちゃってると、パーティションサイズを縮小できない。

注意点その3。この作業は、Windows10が通常モードで動いてる状態で行う。最初、「この手の怪しい作業はセーフモードで行ったほうが安全かなあ」とセーフモードで立ち上げて作業していったら、「セーフモードでこの操作はできないぞ」と拒否られてしまった。

ということで、前述の記事を引用、かつ、DeepLの翻訳結果も載せつつ、作業結果も一応メモしておきます。あくまで自分の場合はこうなったということで…。やってみて何かあっても自己責任ってことで。

1. Open a Command Prompt window (cmd) as admin.
1. 管理者としてコマンドプロンプトウィンドウ(cmd)を開く。

デスクトップ左下の検索欄に「cmd」と打ち込めば「コマンドプロンプト」というアイコンが出てくるので、右クリックして「管理者として実行」を選択。


2. To check the WinRE status, run "reagentc /info" .
2. WinREの状態を確認するには、reagentc /info を実行してください。

If the WinRE is installed, there should be a “Windows RE location” with a path to the WinRE directory.
WinREがインストールされていれば、"Windows RE location "にWinREディレクトリへのパスがあるはずです。

An example is,
例を挙げよう、

"Windows RE location: \\?\GLOBALROOT\device\harddisk0\partition4\Recovery\WindowsRE."

Here, the number after “harddisk” and “partition” is the index of the disk and partition WinRE is on.
ここで、"harddisk "と "partition "の後の数字は、WinREがあるディスクとパーティションのインデックスである。

以下はコマンドを打った際の結果。
>reagentc /info
Windows 回復環境 (Windows RE) およびシステム リセット構成
情報:

    Windows RE の状態:         Enabled
    Windows RE の場所:         \\?\GLOBALROOT\device\harddisk0\partition4\Recovery\WindowsRE
    ブート構成データ (BCD) ID: 73d26c46-9440-11ee-beb5-82ba171add91
    回復イメージの場所:
    回復イメージ インデックス: 0
    カスタム イメージの場所:
    カスタム イメージ インデックス: 0

REAGENTC.EXE: 操作は成功しました。

たしかに、「\\?\GLOBALROOT\device\harddisk0\partition4\Recovery\WindowsRE」という場所が表示された。harddisk 0、partition 4 の位置に WinRE があるようだなと…。


3. To disable the WinRE, run "reagentc /disable"
3. WinREを無効にするには、reagentc /disable を実行します。
>reagentc /disable
REAGENTC.EXE: 操作は成功しました。

>reagentc /info
Windows 回復環境 (Windows RE) およびシステム リセット構成
情報:

    Windows RE の状態:         Disabled
    Windows RE の場所:
    ブート構成データ (BCD) ID: 00000000-0000-0000-0000-000000000000
    回復イメージの場所:
    回復イメージ インデックス: 0
    カスタム イメージの場所:
    カスタム イメージ インデックス: 0

REAGENTC.EXE: 操作は成功しました。

WinREは無効になった模様。


4. Shrink the OS partition and prepare the disk for a new recovery partition.
4. OSパーティションを縮小し、新しいリカバリーパーティション用にディスクを準備する。

a. To shrink the OS, run "diskpart"
a. OSを縮小するには、diskpart を実行します。
>diskpart

Microsoft DiskPart バージョン 10.0.19041.3636

Copyright (C) Microsoft Corporation.
コンピューター: HOGEFUGA

DISKPART>

ディスクのパーティションを操作するツール、diskpart が起動。「DISKPART>」と表示して入力待ちの状態になった。


b. Run "list disk"
b. list disk を実行する。
DISKPART> list disk

  ディスク      状態           サイズ   空き   ダイナ GPT
  ###                                          ミック
  ------------  -------------  -------  -------  ---  ---
  ディスク 0    オンライン           476 GB  1024 KB        *
  ディスク 1    オンライン          1863 GB  1024 KB
  ディスク 2    オンライン           238 GB      0 B

自分のPCは、SSD、HDD、SSD の3つのストレージを繋いであるので、ディスクが3つリストアップされた。


c. To select the OS disk, run "sel disk<OS disk index>"
c. OSディスクを選択するには、"sel disk<OSディスクインデックス>" を実行します。

This should be the same disk index as WinRE.
これは WinRE と同じディスクインデックスでなければならない。
DISKPART> sel disk 0

ディスク 0 が選択されました。

DISKPART> list disk

  ディスク      状態           サイズ   空き   ダイナ GPT
  ###                                          ミック
  ------------  -------------  -------  -------  ---  ---
* ディスク 0    オンライン           476 GB  1024 KB        *
  ディスク 1    オンライン          1863 GB  1024 KB
  ディスク 2    オンライン           238 GB      0 B

「list disk」をしてみると、「ディスク 0」の左側に「*」が表示されている。「現在、ディスク0 が対象になってますよ」という記号なのだろう。この「*」が無かったら、一体どこのディスクを対象にして処理してるのか分からないのでちょっと怖い。


d. To check the partition under the OS disk and find the OS partition, run "list part"
d. OSディスク内のパーティションを確認し、OSパーティションを見つけるには、list part を実行します。
DISKPART> list part

  Partition ###  Type                Size     Offset
  -------------  ------------------  -------  -------
  Partition 1    システム               100 MB  1024 KB
  Partition 2    予約                  16 MB   101 MB
  Partition 3    プライマリ              476 GB   117 MB
  Partition 4    回復                 564 MB   476 GB

各パーティションのサイズからして、Partition 3 がCドライブ(OSが入ってるドライブ)で、Partition 4 が回復パーティションになっている。


e. To select the OS partition, run "sel part<OS partition index>"
e. OSパーティションを選択するには、sel part<OSパーティションインデックス> を実行します。
DISKPART> sel part 3

パーティション 3 が選択されました。

DISKPART> list part

  Partition ###  Type                Size     Offset
  -------------  ------------------  -------  -------
  Partition 1    システム               100 MB  1024 KB
  Partition 2    予約                  16 MB   101 MB
* Partition 3    プライマリ              476 GB   117 MB
  Partition 4    回復                 564 MB   476 GB

Cドライブを選択した。Partition 3 の左側に「*」が表示されている。「このパーティションが対象ですよ」ということだろう。


f. Run "shrink desired=250 minimum=250"
f. shrink desired=250 minimum=250 を実行する。

ここで、shrink というコマンドを使って、Cドライブのパーティションサイズを縮小する。元記事は 250MB ほどCドライブを縮小しているけれど、また似たような不具合に遭遇したら面倒臭いので、この際、回復パーティションを2GBぐらいにしてしまいたい。Cドライブを1500MBほど縮小して、その分を回復パーティションに回してやることにする。

DISKPART> shrink desired=1500 minimum=1500

ボリュームは、次の方法で正常に縮小されました: 1500 MB

SSDを対象にして処理をしたので、体感では数秒で処理が終わってくれた。HDDが対象の場合は…どのくらい時間がかかるのやら…。

余談。セーフモードでこの処理をすると、以下のように拒否られて、先に進めなくなる。Windows10を通常モードで立ち上げた状態で作業すること。
DISKPART> shrink desired=1500 minimum=1500

DiskPart にエラーが発生しました: このサービスはセーフ モードで開始できません
詳細については、システム イベント ログを参照してください。

DISKPART> exit

DiskPart を終了しています...


g. To select the WinRE partition, run "sel part<WinRE partition index>"
g. WinREパーティションを選択するには、sel part<WinREパーティションインデックス> を実行します。
DISKPART> list part

  Partition ###  Type                Size     Offset
  -------------  ------------------  -------  -------
  Partition 1    システム               100 MB  1024 KB
  Partition 2    予約                  16 MB   101 MB
* Partition 3    プライマリ              474 GB   117 MB
  Partition 4    回復                 564 MB   476 GB

DISKPART> sel part 4

パーティション 4 が選択されました。

DISKPART> list part

  Partition ###  Type                Size     Offset
  -------------  ------------------  -------  -------
  Partition 1    システム               100 MB  1024 KB
  Partition 2    予約                  16 MB   101 MB
  Partition 3    プライマリ              474 GB   117 MB
* Partition 4    回復                 564 MB   476 GB

回復パーティションを選択して、削除していく。回復パーティション (Partition 4) の左側に「*」が表示されている。たしかに選択しているぞ、と…。

ここで間違ってCドライブを選択したままだったりすると、Cドライブをまるっと削除することになるので注意。マジで注意。本当に注意。しっかり確認してから先に進もう。マジで。


h. To delete the WinRE partition, run "delete partition override"
h. WinREパーティションを削除するには、delete partition override を実行します。
DISKPART> delete partition override

DiskPart は選択されたパーティションを正常に削除しました。

DISKPART> list part

  Partition ###  Type                Size     Offset
  -------------  ------------------  -------  -------
  Partition 1    システム               100 MB  1024 KB
  Partition 2    予約                  16 MB   101 MB
  Partition 3    プライマリ              474 GB   117 MB

回復パーティションがまるっと全て消滅した。


5. Create a new recovery partition.
5. 新しい回復パーティションを作成する。

a. First, check if the disk partition style is a GUID Partition Table (GPT) or a Master Boot Record (MBR).
a. まず、ディスクのパーティションスタイルがGUIDパーティションテーブル(GPT)かマスターブートレコード(MBR)かを確認します。

To do that, run "list disk".
そのためには、list diskを実行する。

Check if there is an asterisk character (*) in the "Gpt" column.
"Gpt"列にアスタリスク文字(*)があるかチェックする。

If there is an asterisk character (*), then the drive is GPT.
アスタリスク文字(*)がある場合、ドライブはGPTです。

Otherwise, the drive is MBR.
そうでなければ、ドライブはMBRである。
DISKPART> list disk

  ディスク      状態           サイズ   空き   ダイナ GPT
  ###                                          ミック
  ------------  -------------  -------  -------  ---  ---
* ディスク 0    オンライン           476 GB  2065 MB        *
  ディスク 1    オンライン          1863 GB  1024 KB
  ディスク 2    オンライン           238 GB      0 B

表示がずれているけれど、「GPT」のあたりに「*」がついている。つまり、このディスクはGPTで管理されている。


i. If your disk is GPT,
i. ディスクが GPT の場合,

run "create partition primary id=de94bba4-06d1-4d40-a16a-bfd50179d6ac"
create partition primary id=de94bba4-06d1-4d40-a16a-bfd50179d6ac を実行して

followed by the command
というコマンドに続いて

"gpt attributes =0x8000000000000001"
DISKPART> create partition primary id=de94bba4-06d1-4d40-a16a-bfd50179d6ac

DiskPart は指定したパーティションの作成に成功しました。

DISKPART> gpt attributes =0x8000000000000001

選択された GPT パーティションに DiskPart で属性を割り当てました。

今回は GPT のディスクなので、上記のコマンドを打った。MBR の場合は別のコマンドを打つ。ちなみに、回復パーティションには、「de94bba4-06d1-4d40-a16a-bfd50179d6ac」というGUIDをセットする、という取り決めになっているらしい。何にせよ、回復パーティション用のパーティションを作成できた。


ii. If your disk is MBR, run "create partition primary id=27"
ii. ディスクがMBRの場合、create partition primary id=27 を実行する。

今回は、MBRのディスクじゃなかったので、この作業はしなかった。ディスクが MBR なら、このコマンドを打つ。らしい。


b. To format the partition, run 'format quick fs=ntfs label="Windows RE tools"'
b. パーティションをフォーマットするには、format quick fs=ntfs label="Windows RE tools" を実行します。
DISKPART> list part

  Partition ###  Type                Size     Offset
  -------------  ------------------  -------  -------
  Partition 1    システム               100 MB  1024 KB
  Partition 2    予約                  16 MB   101 MB
  Partition 3    プライマリ              474 GB   117 MB
* Partition 4    回復                2065 MB   474 GB

DISKPART> format quicks fs=ntfs label="Windows RE tools"

このコマンドに指定した引数は無効です。
このコマンド タイプの詳細については、「HELP FORMAT」と入力してください。

パーティションを作成しただけでは使えないので、フォーマットをかけるわけだけど。処理が拒否られて焦った。よくよく確認したら、quick と打つべきところを quicks と打っていた。DeepL の翻訳結果が、当初 quick を quicks に書き換えちゃっていたので…。自動翻訳はそういうことが起きたりするから気を付けないといけない。コマンドをコピペする時は、翻訳前の元記事からコピペすること。

DISKPART> FORMAT FS=NTFS LABEL="Windows RE tools" QUICK

  100% 完了しました

DiskPart は、ボリュームのフォーマットを完了しました。

回復パーティションを NTFS でフォーマットできた。


6. To confirm that the WinRE partition is created, run "list vol"
6. WinREパーティションが作成されたことを確認するには、list vol を実行します。
DISKPART> list vol

  Volume ###  Ltr Label        Fs    Type        Size     Status     Info
  ----------  --- -----------  ----  ----------  -------  ---------  --------
  Volume 0     R                       DVD-ROM         0 B  メディアなし
  Volume 1     C                NTFS   Partition    474 GB  正常         ブート
  Volume 2                      FAT32  Partition    100 MB  正常         システム
  Volume 4     D   DATADRV      NTFS   Partition   1863 GB  正常
  Volume 5     X   XDRV         NTFS   Partition    238 GB  正常
* Volume 6         Windows RE   NTFS   Partition   2065 MB  正常         非表示

Volume 6 に、Windows RE が割り当てられてる。パーティションサイズも2GBぐらいになっている。上手く行ってるように見える。


7. To exit from diskpart, run "exit"
7. diskpart を終了するには、exit を実行します。
DISKPART> exit

DiskPart を終了しています...

diskpart を抜けた。


8. To re-enable WinRE, run "reagentc /enable"
8. WinREを再度有効にするには、reagentc /enable を実行します。
>reagentc /enable
REAGENTC.EXE: 操作は成功しました。

WinREを有効にしたぞ、と。


9. To confirm where WinRE is installed, run "reagentc /info"
9. WinREがどこにインストールされているかを確認するには、reagentc /info を実行してください。
>reagentc /info
Windows 回復環境 (Windows RE) およびシステム リセット構成
情報:

    Windows RE の状態:         Enabled
    Windows RE の場所:         \\?\GLOBALROOT\device\harddisk0\partition4\Recovery\WindowsRE
    ブート構成データ (BCD) ID: c072bb99-b00d-11ee-a1fc-a01a7839d147
    回復イメージの場所:
    回復イメージ インデックス: 0
    カスタム イメージの場所:
    カスタム イメージ インデックス: 0

REAGENTC.EXE: 操作は成功しました。

元の状態になっているぞ、と。

これで、回復パーティションのサイズを増やすことができた。たぶん。


Note: If creation failed or you did not want to extend the WinRE partition,
注:作成に失敗した場合、またはWinREパーティションを拡張したくない場合、

run "reagentc /enable" to re-enable WinRE.
reagentc /enable を実行し、WinREを再有効化する。

途中で元の状態に戻したくなった時は、reagentc /enable を打って、WinRE を有効にすればいい。実際、セーフモードで作業して失敗した時は、以下を打って元に戻したりもした。
>reagentc /enable
REAGENTC.EXE: 操作は成功しました。

>reagentc /info
Windows 回復環境 (Windows RE) およびシステム リセット構成
情報:

    Windows RE の状態:         Enabled
    Windows RE の場所:         \\?\GLOBALROOT\device\harddisk0\partition4\Recovery\WindowsRE
    ブート構成データ (BCD) ID: c072bb97-b00d-11ee-a1fc-a01a7839d147
    回復イメージの場所:
    回復イメージ インデックス: 0
    カスタム イメージの場所:
    カスタム イメージ インデックス: 0

REAGENTC.EXE: 操作は成功しました。

さておき、この作業をしたメインPC上では、件の WindowsUpdateエラーが解消された。これでなんとかなったかもしれない…。

余談。この状況はヤバい :

一応作業できたっぽいので一安心したけれど。どう考えてもこんな作業、そのへんの一般的なWindows10ユーザができるとは思えないのですが…。少なくともウチの親父さんはこんな作業を絶対にできない…。

間違って作業したらOSがまるっと削除されたりするから、この状況はとてもヤバい気がする…。

2024/01/12追記 :

Windows10 x64 22H2をインストールしてあるサブPCでも、WindowsUpdateで同じエラーが出て、回復パーティションサイズも同じように500MB+αしかなかったので、同じ作業をして回復パーティションサイズを2GBにしてみた。

サブPCのディスクはGPTじゃなくてMBRだったけど、件の記事通りにMBR用のコマンドを打ったら回復パーティションを再作成することができた。

WindowsUpdateのエラーも出なくなった。少なくとも自分の環境では、回復パーティションサイズが足りないことが原因で件のエラーが出ていたようだなと…。

2024/01/12(金) [n年前の日記]

#1 [prog] OpenGLで文字描画をしたい

先日書いた、OpenGLを使って宇宙飛行スクリーンセーバっぽい描画をするプログラムを、せっかくだからスクリーンセーバにしたい。ただ、FPSも描画したいなと。そのためには OpenGL で文字描画をしないといけない。

OpenGL には、文字描画用の便利な機能は標準で用意されていない。そのあたりをどうにかしないといけない。ググったところ、色々方法はあるようで…。
今までは glut/freeglut を使って OpenGL のウインドウを表示していたので、glutBitmapCharacter() を使ってFPS値を描画していたのだけど。スクリーンセーバにする際に、glut を含めることができるのか、ちょっとよく分からなくて…。別の方法で文字描画することになるのかなと…。

wglUseFontBitmapsを使ってみる :

Windowsに特化したプログラムになってしまうけど、まずは wglUseFontBitmaps() を使って描画できそうか試してみた。そもそも、Windows用のスクリーンセーバを書く時点で、Windows に特化してしまうわけだし…。

以下のページを参考にさせてもらった。ありがたや。

_文字列描画 - OpenGLプログラミングメモ - atwiki(アットウィキ)

元記事のソースは日本語表示ができるようになっているけれど、手元の環境が MinGW(gcc/g++) だったせいか、そのままではビルドできなかったので、英数字のみ(ASCIIのみ)対応させるように修正して実験してみた。 *1

環境は、Windows10 x64 22H2 + MinGW (gcc 6.3.0) or MSYS2 MINGW64 (gcc 13.2.0) + freeglut。

_01_drawtextwgl.cpp
// draw text on OpenGL for wglUseFontBitmaps

#include <GL/freeglut.h>
#include <stdio.h>
#include <tchar.h>

#define WIDTH 512
#define HEIGHT 288

static int count = 0;

class GLFONT
{
public:
  HFONT Hfont;
  HDC Hdc;

  // create font
  GLFONT(LPCTSTR fontname, int size)
  {
    Hfont = CreateFont(
        size,                    // font height
        0,                       // font width
        0,                       // テキストの角度
        0,                       // Orientation
        FW_REGULAR,              // font weight
        FALSE,                   // italic
        FALSE,                   // underline
        FALSE,                   // strikeout
        ANSI_CHARSET,            // charset
        OUT_DEFAULT_PRECIS,      // OutPrecision
        CLIP_DEFAULT_PRECIS,     // ClipPrecision
        ANTIALIASED_QUALITY,     // Quality
        FIXED_PITCH | FF_MODERN, // Pitch And Family
        fontname                 // face name
    );

    Hdc = wglGetCurrentDC();
    SelectObject(Hdc, Hfont);
  }

  // draw string
  void DrawString(int x, int y, char *format, ...)
  {
    int Length = 0;
    int list = 0;

    if (format == NULL)
      return;

    Length = strlen(format);
    list = glGenLists(Length);

    for (int i = 0; i < Length; i++)
    {
      wglUseFontBitmaps(Hdc, format[i], 1, list + i);
    }

    glDisable(GL_LIGHTING);
    glRasterPos2i(x, y);

    // draw
    for (int i = 0; i < Length; i++)
    {
      glCallList(list + i);
    }

    glEnable(GL_LIGHTING);

    // delete displey list
    glDeleteLists(list, Length);

    list = 0;
    Length = 0;
  }
};

GLFONT *font;

// Draw OpenGL
void display(void)
{
  glClearColor(0.2, 0.4, 0.8, 0.0);
  glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);

  // draw text
  glColor4f(1.0, 1.0, 1.0, 1.0); // set text color

  font->DrawString(40, 40, (char *)"Hello World");

  {
    char buf[256];
    sprintf(buf, "count %d", count);
    font->DrawString(40, 100, buf);
  }

  glutSwapBuffers();
  count++;
}

void idle(void)
{
  glutPostRedisplay(); // redraw
}

void keyboard(unsigned char key, int x, int y)
{
  switch (key)
  {
  case '\x1B':
  case 'q':
    // Exit on escape or 'q' key press
    glutLeaveMainLoop();
    // exit(EXIT_SUCCESS);
    break;
  }
}

int main(int argc, char *argv[])
{
  glutInit(&argc, argv);
  glutInitDisplayMode(GLUT_RGBA | GLUT_DOUBLE);
  glutInitWindowSize(WIDTH, HEIGHT);
  // glutInitWindowPosition(100, 100);

  glutCreateWindow("Draw string");

  glutKeyboardFunc(keyboard);
  glutDisplayFunc(display);
  glutIdleFunc(idle);

  glOrtho(0, WIDTH, HEIGHT, 0, -1, 1);
  font = new GLFONT(_T("Courier New"), 24);

  glutMainLoop();
  return 0;
}

_Makefile
ifeq ($(MSYSTEM),MINGW64)

# --------------------
# MSYS2 MINGW64 (gcc 13.2.0)

01_drawtextwgl.exe: 01_drawtextwgl.cpp Makefile
    g++ $< -o $@ -lglu32 -D FREEGLUT_STATIC -lfreeglut -lopengl32 -lwinmm -lgdi32 -static

else

# --------------------
# MinGW (gcc 6.3.0)

01_drawtextwgl.exe: 01_drawtextwgl.cpp Makefile
    g++ $< -o $@ -lglu32 -D FREEGLUT_STATIC -lfreeglut_static -lopengl32 -lwinmm -lgdi32

endif

.PHONY: clean
clean:
    rm -f *.exe
    rm -f *.o

make でビルド。01_drawtextwgl.exe を実行した結果は以下。

01_drawtextwgl_ss.gif

たしかに描画できてる。文字列の内容が変化する場合でも描画できている模様。

ハマった点は…。フォント種類を変えてみても反映されなくて悩んだけれど、CreateFont() に与える CHARSET が SJIS_CHARSET のままになっていたのが原因だった。英数字のみのフォントを使いたい時は、ASCII_CHARSET を指定しないといかんのだな…。

フォント名等を渡すときに警告が出てきて悩んだけれど、あちこちのサンプルを真似して _T("xxxx") で囲んだら警告が出なくなった。何をしているマクロ(?)なのかよく分かってない…。

日本語文字列を描画したい時は wchar_t とやらが絡んでくるらしいけど、今回は英数字のみ描画できればいいので、char に書き換えてしまった。日本語表示については今後の課題ということで…。

*1: 考えてみたら、Windows上でしか動かないプログラムを書くのだから、フツーは Visual C++ を使うよなと…。Visual C++ が有償だった昔と違って、今は誰でも Visual C++ を利用できる状況になっているわけだし…。gcc/g++ で試していること自体、「え? なんでわざわざそんなものを使うの?」と言われそうな気もする…。

2024/01/13() [n年前の日記]

#1 [prog] OpenGL + glBitmapで文字描画をしたい

OpenGL で文字描画をしたい。 _昨日の実験 で、wglUseFontBitmaps() を使えば文字描画ができることは分かったけれど、Windowsに特化しているあたりがなんだかちょっと微妙になんとなく気になる。ここは一つ、Linux あたりでも使えそうな方法でやれないものか。

そんなわけで、2値画像を描画できるという glBitmap() を使えば、Windows以外でも使えるのではないかと思えてきたので、そのあたりを試してみた。

glBitmapで画像を描画 :

まずは glBitmap() の基本的な使い方を把握したい。以下のページが参考になった。ありがたや。

_ビットマップ


その前に、画像をビットパターンの配列に変換するツールが欲しいなと…。png画像を読み込んで、unsigned char の配列にして出力する Pythonスクリプトを書いてみた。環境は、Windows10 x64 22H2 + Python 3.10.10 64bit + Pillow 10.1.0。

_png2bits.py
"""
png image to c header file

Usage: python png2bits.py -i input.png --label label_name > image.h

Windows10 x64 22H2 + Python 3.10.10 64bit + Pillow 10.1.0
"""

import argparse
import os
import sys
from PIL import Image


def main():
    parser = argparse.ArgumentParser()
    parser.add_argument("-i", "--infile", required=True, help="PNG image file")
    parser.add_argument("--label", help="symbol name")
    args = parser.parse_args()
    infile = args.infile

    if os.path.isfile(infile):
        print("/* infile: %s */" % infile)
    else:
        print("Error: Not found %s" % infile)
        sys.exit()

    if not args.label:
        label = infile.replace(".", "_")
        label = label.replace(" ", "_")
    else:
        label = args.label

    im = Image.open(infile).convert("L")
    im.point(lambda x: 0 if x < 128 else x)
    w, h = im.size
    print("static unsigned int %s_width = %d;" % (label, w))
    print("static unsigned int %s_height = %d;\n" % (label, h))
    print("static unsigned char %s[] = {" % (label))

    count = 0
    for y in range(h - 1, -1, -1):
        buf = []
        for i in range(int(w / 8)):
            buf.append(0)
        if (w % 8) != 0:
            buf.append(0)

        for x in range(w):
            v = im.getpixel((x, y))
            if v > 128:
                buf[int(x / 8)] |= 1 << (7 - (x % 8))

        s = ""
        for v in buf:
            s += "0x%s," % format(v, "02x")
            count += 1
        print("  %s // %d" % (s, y))

    print("};\n")

    print("static unsigned int %s_len = %d;" % (label, count))


if __name__ == "__main__":
    main()

使い方は以下。
python png2bits.py -i INPUT.png --label LABEL_NAME > OUTPUT.h

やってることは、PIL(Pillow)でpng画像を読み込んでグレースケール画像に変換して、各ドットをチェックして値が128より大きければ1に、小さければ0にして、8ドット=1byteにまとめて並べて出力しているだけ。OpenGLは下から上に画像を描画していくらしいので、png画像をスキャンしていくときも下から上に見ていくようにした。

このスクリプトを使って、以下の画像を C言語の .h の形に変換。

image_32x32.png
_image_32x32.png

image_lena.png
_image_lena.png

以下のような .h が得られた。

_image_32x32.h
_image_lena.h

内容は以下のような感じ。
/* infile: image_32x32.png */
static unsigned int image_32x32_width = 32;
static unsigned int image_32x32_height = 32;

static unsigned char image_32x32[] = {
  0xff,0xff,0x00,0x00, // 31
  0xff,0xff,0x00,0x00, // 30

// ...

  0x01,0x80,0x55,0xcc, // 1
  0x00,0x00,0xaa,0xcc, // 0
};


これで描画すべきビットパターンの配列は得られた。OpenGL の glBitmap() で描画するプログラムを書いてみる。

ビルドは、Windows10 x64 22H2上で、MinGW (gcc 6.3.0) / MSYS2 MINGW64 (gcc 13.2.0) + freeglut を使った。

_01_glbitmap.c
// draw bitmap sample on OpenGL

#include <windows.h>
#include <stdio.h>
#include <GL/gl.h>
#include <GL/freeglut.h>

// include bitmap pattern
#include "image_32x32.h"
#include "image_lena.h"

void display(void)
{
  GLfloat xorig, yorig, xmove, ymove;
  GLsizei w, h;

  glClearColor(0.0, 0.0, 0.0, 0.0);
  glClear(GL_COLOR_BUFFER_BIT);

  glPixelStorei(GL_UNPACK_ALIGNMENT, 1);

  // draw arrow
  glColor4f(1, 1, 1, 1);     // set color
  glRasterPos2f(-0.5, -0.5); // set position
  xorig = 0.0;
  yorig = 0.0;
  xmove = 0.0;
  ymove = 0.0;
  w = image_32x32_width;
  h = image_32x32_height;
  glBitmap(w, h, xorig, yorig, xmove, ymove, image_32x32); // draw bitmap

  // draw lena
  glColor4f(1.0, 0.8, 0.5, 1);
  glRasterPos2f(0.0, 0.0);
  xorig = 0.0;
  yorig = 0.0;
  xmove = 0.0;
  ymove = 0.0;
  w = image_lena_width;
  h = image_lena_height;
  glBitmap(w, h, xorig, yorig, xmove, ymove, image_lena);

  glFlush();
}

void keyboard(unsigned char key, int x, int y)
{
  switch (key)
  {
  case '\x1B':
  case 'q':
    // Exit on escape or 'q' key press
    glutLeaveMainLoop();
    // exit(EXIT_SUCCESS);
    break;
  }
}

int main(int argc, char **argv)
{
  glutInit(&argc, argv);
  glutInitWindowSize(512, 288);
  glutInitDisplayMode(GLUT_SINGLE | GLUT_RGBA | GLUT_DEPTH);

  glutCreateWindow("glBitmap sample");
  glutKeyboardFunc(keyboard);
  glutDisplayFunc(display);

  glutMainLoop();
  return EXIT_SUCCESS;
}

_Makefile
PROGRAM = 01_glbitmap.exe
SRC = $(PROGRAM:%.exe=%.c)

ifeq ($(MSYSTEM),MINGW64)

# --------------------
# MSYS2 MINGW64

$(PROGRAM): $(SRC) image_32x32.h image_lena.h  Makefile
    gcc $< -o $@ -lglu32 -D FREEGLUT_STATIC -lfreeglut -lopengl32 -lwinmm -lgdi32 -static

else

# --------------------
# MinGW

$(PROGRAM): $(SRC) image_32x32.h image_lena.h  Makefile
    gcc $< -o $@ -lglu32 -D FREEGLUT_STATIC -lfreeglut_static -lopengl32 -lwinmm -lgdi32

endif

image_32x32.h: image_32x32.png Makefile
    python png2bits.py -i $< --label image_32x32 > $@

image_lena.h: image_lena.png Makefile
    python png2bits.py -i $< --label image_lena > $@

.PHONY: clean
clean:
    rm -f *.exe
    rm -f *.o
    rm -f image_32x32.h
    rm -f image_lena.h

make と打って、01_glbitmap.exe が生成された。実行結果は以下。

01_glbitmap_ss.png

両方とも描画できた。

glBitmapで文字描画をしてみる :

glBitmap() を使えば2値画像を描画できることが分かったので、コレを使って文字描画ができないか試してみる。ひとまず、英数字のみを等幅フォントで描画する方向で考える。

その前に、フォントデータを作らないといけない…。コレも、Python を使って、フォントを敷き詰めた画像からビットパターンを作って、unsigned char 配列として出力するスクリプトを書いてみた。環境は、Windows10 x64 22H2 + Python 3.10.10 64bit + Pillow 10.1.0。

_fontpng2bits.py
"""
font png image to c header file

Usage: python fontpng2bits.py -i input.png --label label_name > image.h

Windows10 x64 22H2 + Python 3.10.10 64bit + Pillow 10.1.0
"""

import argparse
import os
import sys
from PIL import Image


def main():
    parser = argparse.ArgumentParser()
    parser.add_argument("-i", "--infile", required=True, help="PNG image file")
    parser.add_argument("--label", help="symbol name")
    args = parser.parse_args()
    infile = args.infile

    if os.path.isfile(infile):
        print("/* infile: %s */" % infile)
    else:
        print("Error: Not found %s" % infile)
        sys.exit()

    if not args.label:
        label = infile.replace(".", "_")
        label = label.replace(" ", "_")
    else:
        label = args.label

    im = Image.open(infile).convert("L")
    im.point(lambda x: 0 if x < 128 else x)
    imgw, imgh = im.size
    w = int(imgw / 16)
    h = int(imgh / 6)
    print("/* source image size = %d x %d */\n" % (imgw, imgh))

    print("static int %s_width = %d;" % (label, w))
    print("static int %s_height = %d;" % (label, h))

    alen = int(w / 8) + (1 if (w % 8) != 0 else 0)
    clen = alen * h
    cnum = 16 * 6
    print("static int %s_chr_len = %d;\n" % (label, clen))

    print("static unsigned char %s[%d][%d] = {" % (label, cnum, clen))

    for c in range(16 * 6):
        bx = int(c % 16) * w
        by = int(c / 16) * h

        print("  {")

        print("    // code = 0x%02x" % (c + 0x20))

        for y in range(h - 1, -1, -1):
            buf = []
            for i in range(alen):
                buf.append(0)

            for x in range(w):
                v = im.getpixel((bx + x, by + y))
                if v > 128:
                    buf[int(x / 8)] |= 1 << (7 - (x % 8))

            s = ""
            for v in buf:
                s += "0x%s," % format(v, "02x")
            print("    %s" % s)

        print("  },")

    print("};")


if __name__ == "__main__":
    main()

使い方は以下。
python fontpng2bits.py -i INPUT.png --label LABEL_NAME > OUTPUT.h


フォント画像は以下のものを用意した。ASCIIコード 0x20 - 0x7f までを、横16文字、縦6行に、等幅で敷き詰めた画像を用意する。各フォントの入手先については後述する。

font_pet2015.png
_font_pet2015.png

font_courR18.png
_font_courR18.png

font_shnm8x16r.png
_font_shnm8x16r.png

font_shnm8x16rx2.png
_font_shnm8x16rx2.png

font_profont.png
_font_profont.png

font_ter-u24b.png
_font_ter-u24b.png


これを先ほどのPythonスクリプトに渡して、C言語の .h(ヘッダーファイル)の形にした。

_fontdata.h
_fontdata_courr18.h
_fontdata_shnm8x16r.h
_fontdata_shnm8x16rx2.h
_fontdata_profont.h
_fontdata_ter-u24b.h

中身は以下のような感じになっている。文字1つ毎に配列になってる、2次元配列として用意した。
/* infile: font_pet2015.png */
/* source image size = 256 x 96 */

static int fontdata_width = 16;
static int fontdata_height = 16;
static int fontdata_chr_len = 32;

static unsigned char fontdata[96][32] = {
  {
    // code = 0x20
    0x00,0x00,
    0x00,0x00,
    0x00,0x00,
    0x00,0x00,

// ...

    0x00,0x00,
    0x00,0x00,
  },
  {
    // code = 0x21
    0x00,0x00,

// ...

    0x0c,0x30,
    0x03,0xc0,
    0x03,0xc0,
  },
};


フォントデータが用意できたので、glBitmap() で描画してみる。ソースは以下。文字描画の処理をしているのは、drawText() の部分。

環境は、Windows10 x64 22H2 + MinGW (gcc 6.3.0) / MSYS2 MINGW64 (gcc 13.2.0) + freeglut。

_02_drawbitmapfont.c
// bitmap font draw sample on OpenGL

#include <windows.h>
#include <stdio.h>
#include <string.h>
#include <GL/gl.h>
#include <GL/freeglut.h>

// include bitmap pattern
// --------------------
// #include "fontdata.h"
// #include "fontdata_courr18.h"
// #include "fontdata_shnm8x16r.h"
// #include "fontdata_shnm8x16rx2.h"
// #include "fontdata_profont.h"
#include "fontdata_ter-u24b.h"

#define SCRW 512
#define SCRH 288

static int framerate = 60;

static int count = 0;

// draw text
void drawText(char *str)
{
  GLsizei w = (GLsizei)fontdata_width;
  GLsizei h = (GLsizei)fontdata_height;
  int clen = fontdata_chr_len;
  GLfloat xorig, yorig;
  GLfloat xmove, ymove;
  xorig = 0;
  yorig = 0;
  xmove = w;
  ymove = 0;

  glPixelStorei(GL_UNPACK_ALIGNMENT, 1); // コレが無いと表示が崩れる

  int slen = strlen(str);
  for (int i = 0; i < slen; i++)
  {
    int c = str[i];
    if (c == 0)
      break;
    if (c < 0x20 || c > 0x7f)
      c = 0x20;
    c -= 0x20;
    glBitmap(w, h, xorig, yorig, xmove, ymove, &(fontdata[c][0]));
  }
}

// draw OpenGL
void display(void)
{
  GLfloat xorig, yorig, xmove, ymove;
  GLsizei w, h;

  glClearColor(0.0, 0.0, 0.0, 0.0);
  glClear(GL_COLOR_BUFFER_BIT);

  // draw text
  glColor4f(1, 1, 1, 1);    // set color
  glRasterPos2f(-0.8, 0.5); // set position
  drawText("Hello World !!");

  {
    char buf[256];
    sprintf(buf, "count %d", count);
    glRasterPos2f(-0.8, 0.0);
    drawText(buf);
  }

  glutSwapBuffers();
  glFlush();

  count++;
}

void onTimer(int value)
{
  glutPostRedisplay(); // redraw
  glutTimerFunc((1000 / framerate), onTimer, 0);
}

void keyboard(unsigned char key, int x, int y)
{
  switch (key)
  {
  case '\x1B':
  case 'q':
    // Exit on escape or 'q' key press
    glutLeaveMainLoop();
    // exit(EXIT_SUCCESS);
    break;
  }
}

int main(int argc, char **argv)
{
  count = 0;
  
  glutInit(&argc, argv);
  glutInitWindowSize(SCRW, SCRH);
  glutInitDisplayMode(GLUT_DOUBLE | GLUT_RGBA | GLUT_DEPTH);

  glutCreateWindow("bitmap font draw sample");
  glutKeyboardFunc(keyboard);
  glutDisplayFunc(display);

  glutTimerFunc((1000 / framerate), onTimer, 0);

  glutMainLoop();
  return EXIT_SUCCESS;
}

最初、表示がグチャグチャになってハマってしまった。glPixelStorei(GL_UNPACK_ALIGNMENT, 1); を入れないと思った通りに描画されなかった。


_Makefile
PROGRAM = 02_drawbitmapfont.exe
SRC = $(PROGRAM:%.exe=%.c)
FONTS = fontdata.h fontdata_courr18.h fontdata_shnm8x16r.h fontdata_shnm8x16rx2.h fontdata_profont.h fontdata_ter-u24b.h

ifeq ($(MSYSTEM),MINGW64)

# --------------------
# MSYS2 MINGW64

$(PROGRAM): $(SRC) $(FONTS) Makefile
    gcc $< -o $@ -lglu32 -D FREEGLUT_STATIC -lfreeglut -lopengl32 -lwinmm -lgdi32 -static

else

# --------------------
# MinGW

$(PROGRAM): $(SRC) $(FONTS) Makefile
    gcc $< -o $@ -lglu32 -D FREEGLUT_STATIC -lfreeglut_static -lopengl32 -lwinmm -lgdi32

endif

fontdata.h: font_pet2015.png fontpng2bits.py Makefile
    python fontpng2bits.py -i $< --label fontdata > $@

fontdata_courr18.h: font_courR18.png fontpng2bits.py Makefile
    python fontpng2bits.py -i $< --label fontdata > $@

fontdata_shnm8x16r.h: font_shnm8x16r.png fontpng2bits.py Makefile
    python fontpng2bits.py -i $< --label fontdata > $@

fontdata_shnm8x16rx2.h: font_shnm8x16rx2.png fontpng2bits.py Makefile
    python fontpng2bits.py -i $< --label fontdata > $@

fontdata_profont.h: font_profont.png fontpng2bits.py Makefile
    python fontpng2bits.py -i $< --label fontdata > $@

fontdata_ter-u24b.h: font_ter-u24b.png fontpng2bits.py Makefile
    python fontpng2bits.py -i $< --label fontdata > $@

.PHONY: clean
clean:
    rm -f *.exe
    rm -f *.o
    rm -f fontdata.h

Makefile には、先ほど用意した全フォントデータ(.h) を生成する行も記述しちゃってるけど、不要なら該当行は消してしまって構わない。

make でビルド。得られた 02_drawbitmapfont.exe を実行すると以下のような結果になった。

ss_fontdata.png

文字(文字列)を描画することができた。

ソースの最初のほうのコメントアウトを修正して、別のフォントを描画してみると、以下のような感じになる。印象が結構違うなと…。

courR18.bdf の例。
ss_fontdata_courr18.png

東雲フォント(shnm8x16r.bdf)の例。
ss_fontdata_shnm8x16r.png

東雲フォント(shnm8x16r.bdf)を2倍に拡大した例。
ss_fontdata_shnm8x16rx2.png

ProFontの例。
ss_fontdata_profont.png

Terminus Font の例。
ss_fontdata_ter-u24b.png


ということで、glBitmap() を使って、OpenGL で文字描画をすることができる、と分かった。

余談。これらのフォントデータと、glBitmap() を使って描画する関数を、一つの .h(ヘッダーファイル)にまとめてしまって、「この .h を1つ #include すれば文字描画できますよ」という形にしてしまえば少しは便利に使えるかもしれないなと…。

もっとも…。
  • glut を使っていたら、既に文字描画できる関数 glutBitmapCharacter() があったりするし。
  • Windowsに特化すれば wglUseFontBitmaps() があるし。
  • Linux なら FTGLライブラリがあるし。
学習用途ならそれらを使うほうが手っ取り早いか…。

フォントの入手先 :

前述のように、ASCIIコード 0x20 - 0x7f を、横16文字 x 縦6行で、等幅で並べた画像さえあれば、こうしてフォントデータを用意できるので、元になるフォントは画像でもbdfフォントでもなんでも使えるはず。要は、以下の文字さえ用意してあればいい。

 !"#$%&'()*+,-./
0123456789:;<=>?
@ABCDEFGHIJKLMNO
PQRSTUVWXYZ[\]^_
`abcdefghijklmno
pqrstuvwxyz{|}~ 


とりあえず、今回用意したフォント画像の元フォントの入手先をメモしておく。


一番最初に使った pet2015 というフォントは、自分が作ったビットマップフォント。ライセンスは CC0 / Public Domain。画像しか存在していない。1文字8x8ドット。8bit PC MZ-700のフォントに似せつつドット単位では別物になるようにドットを打った。 *1

_mieki256's diary - HGIMG3とビットマップフォント画像の作り方を勉強中


courR18.bdf は、ググったら Apple のサイトにあったのでそこから入手。bdfファイルの最初のほうにライセンスその他が書いてあると思うのだけど、英語分からん…。実験用なら使ってもいいのではないかと安易に思って使ってみたけど、実際はどうなんだろう…。

_courR18.bdf


東雲フォントは以下から入手。ライセンスは Public Domain。shinonome-0.9.11p1.tar.bz2 を入手して解凍。bdfフォルダの中に .bdfフォントが入っている。英数字部分は、6x12, 7x14, 8x16, 9x18 が用意されてる。

_shinonome font family


ProFont は以下から入手。ライセンスは _MIT License になっているらしい。profont-x11.zip を入手して解凍。pcfフォントしか入ってない。*.pcf を任意のフォルダに置いて、GIMP 2.8.22 Portable のフォントフォルダとして追加して、GIMP上で画像化してみた。 *2

_ProFont for Windows, for Macintosh, for Linux


Terminus Font は以下から入手。ライセンスは SIL Open Font License, Version 1.1。terminus-font-4.49.1.tar.gz を入手して解凍。bdfフォントが入っている。

_Terminus Font Home Page

bdfフォントの画像化 :

bdfフォントを画像化したい時は、bdf2bmp というツールが使える。

_bdf2bmp

使い方は以下。
bdf2bmp input.bdf output.bmp

デフォルトだと境界線(?)が入るけれど、オプションをつければ、境界線を入れない状態で出力できたり、横に並ぶ文字の個数を指定できたりする。以下は、境界線無し(0ドット幅)、横に16文字並べる指定。
bdf2bmp input.bdf output.bmp -s0 -c16

こうして画像化してしまえば、任意のグリッドサイズを指定、かつ、グリッド単位で選択や移動ができる画像編集ソフトを使うことで、文字の並び替えもできるかなと。自分の場合は _ドットエディタ EDGE2 (シェアウェア) で作業してしまうことが多いけれど、GIMP のグリッド表示と吸着を有効化して作業できなくもないかなと…。

*1: 余談。MZシリーズのフォントは PET 2001 というPCのフォントとクリソツで…。当時はなんというか、大らかだったんだなと…。
*2: _最近のバージョンのGIMPは一部のビットマップフォントが使えない という話があったので、今回、念のために、あえて GIMP 2.8.22 Portable を使って作業した。

2024/01/14() [n年前の日記]

#1 [prog] C言語でiniファイルの読み書きをしたい

C/C++ と OpenGL を使って、Windows用のスクリーンセーバを作りたい。しかし、スクリーンセーバの設定パラメータを、どこにどうやって保存しようか…。

一般的なスクリーンセーバは、えてしてWindowsのレジストリに設定を記録するらしいけど、レジストリの読み込みはともかく、書き込みなんて怖くてやりたくない。ここは一つ、iniファイルの読み書きで勘弁してもらえないだろうか。

でも、C/C++ でiniファイルの読み書きってどうやったらいいんだろう。ということでそのあたりを調べて実験してみた。

環境は、Windows10 x64 22H2 + MinGW (gcc 6.3.0)。

WritePrivateProfileStringを使う :

ググったところ、iniファイルの読み書きについては、Windows なら WritePrivateProfileString()、GetPrivateProfileString() といった関数が使えるらしい。

_INIファイル(Win32API)(C言語) - 超初心者向けプログラミング入門
_iniファイルを読み込む - わびさびサンプルソース

Windowsに特化してるあたりがなんだかちょっと気になるけれど、これがもし Linux なら、ini ファイルではなく .xxxxrc 等のファイルに設定を保存しそうではあるし、iniファイルにアクセスする時点で、そのプログラムは Windows上でしか動かさないやろ、という気もするのでこの際使ってしまうことにした。

そんな感じで、こうなった。実行ファイルと同じ場所(ディレクトリ)に test.ini が無かったら作成して、その test.ini の内容を読み込んで printf() で出力するだけのプログラム。

_01_write_ini.c
// ini file read and write sample

#include <stdio.h>
#include <stdlib.h>
#include <shlwapi.h>
#include <windows.h>

#define INIFILENAME "test.ini"
#define SECNAME "ssstarsgl_config"

// global work
int wait = 15;
int speed = 1000;
int number = 500;
int fps_disp_enable = 0;

int main(void)
{
  char cdir[MAX_PATH];
  char filepath[MAX_PATH];

  // get current directory
  GetCurrentDirectory(MAX_PATH, cdir);

  // create save file path
  PathCombine(filepath, cdir, INIFILENAME);

  printf("Current Directory : %s\n", cdir);
  printf("ini file path : %s\n\n", filepath);

  if (!PathFileExists(filepath))
  {
    // Not found ini file
    printf("Not found %s\n", filepath);
    printf("Create %s\n\n", filepath);

    char buf[256];

    // create/write ini file
    sprintf(buf, "%d", wait);
    WritePrivateProfileString(SECNAME, "wait", buf, filepath);

    sprintf(buf, "%d", speed);
    WritePrivateProfileString(SECNAME, "speed", buf, filepath);

    sprintf(buf, "%d", number);
    WritePrivateProfileString(SECNAME, "number", buf, filepath);

    sprintf(buf, "%d", fps_disp_enable);
    WritePrivateProfileString(SECNAME, "fps_disp_enable", buf, filepath);
  }

  if (!PathFileExists(filepath))
  {
    printf("Not found %s\n\n", filepath);
    return -1;
  }
  else
  {
    printf("Found %s\n", filepath);

    // read ini file
    printf("Read ini file\n");
    wait = GetPrivateProfileInt(SECNAME, "wait", -1, filepath);
    speed = GetPrivateProfileInt(SECNAME, "speed", -1, filepath);
    number = GetPrivateProfileInt(SECNAME, "number", -1, filepath);
    fps_disp_enable = GetPrivateProfileInt(SECNAME, "fps_disp_enable", -1, filepath);

    // dump results
    printf("wait  = %d\n", wait);
    printf("speed = %d\n", speed);
    printf("number = %d\n", number);
    printf("fps_disp_enable = %d\n", fps_disp_enable);
  }

  return 0;
}

_Makefile
01_write_ini.exe: 01_write_ini.c
    gcc $< -o $@ -lshlwapi -Wall -O3

.PHONY: clean
clean:
    rm -f *.exe
    rm -f *.o
    rm -f test.ini

make でビルドして 01_write_ini.exe を作る。

DOS窓(cmd.exe)上で 01_write_ini.exe を実行すると、以下のような出力をして、test.ini という iniファイルも作成された。
> 01_write_ini.exe
Current Directory : D:\home\prg\c_lang\gcc\ini_read_write\take1
ini file path : D:\home\prg\c_lang\gcc\ini_read_write\take1\test.ini

Not found D:\home\prg\c_lang\gcc\ini_read_write\take1\test.ini
Create D:\home\prg\c_lang\gcc\ini_read_write\take1\test.ini

Found D:\home\prg\c_lang\gcc\ini_read_write\take1\test.ini
Read ini file
wait  = 15
speed = 1000
number = 500
fps_disp_enable = 0

test.ini の内容は以下のような感じ。

_test.ini
[ssstarsgl_config]
wait=15
speed=1000
number=500
fps_disp_enable=0

再度実行すると、test.ini が存在するので、test.ini を読み込んで内容の表示だけをする。
> 01_write_ini.exe
Current Directory : D:\home\prg\c_lang\gcc\ini_read_write\take1
ini file path : D:\home\prg\c_lang\gcc\ini_read_write\take1\test.ini

Found D:\home\prg\c_lang\gcc\ini_read_write\take1\test.ini
Read ini file
wait  = 15
speed = 1000
number = 500
fps_disp_enable = 0

少し解説 :

分かった範囲で、少し解説。

  • 実行している exeファイルの場所(ディレクトリ)を取得するには、GetCurrentDirectory() を使う。
  • ディレクトリのPATHに、ファイル名を結合したい時は、PathCombine() を使う。

_Win32APIでパス名とファイル名を連結する - プログラムを書こう!
_PathCombine

  char filepath[MAX_PATH];

  // get current directory
  GetCurrentDirectory(MAX_PATH, cdir);

  // create save file path
  PathCombine(filepath, cdir, INIFILENAME);

  printf("Current Directory : %s\n", cdir);
  printf("ini file path : %s\n\n", filepath);


ファイルの存在チェックは、PathFileExists() が使える。

_PathFileExists - Windows APIの部屋

  if (!PathFileExists(filepath))
  {
    // Not found ini file
    // ...
  }
  else
  {
    // Found ini file
    // ...
  }

注意点。PathCombine() や PathFileExists() を使うには、#include <shlwapi.h> が必要らしい。また、gcc/g++ に -lshlwapi を渡してライブラリをリンクしないといけない。


iniファイルへの書き込みは、WritePrivateProfileString() を使う。もし、指定したiniファイルが存在しなかったら、自動でiniファイルを作成して書き込んでくれるらしい。
WritePrivateProfileString("セクション名", "キー名", "キーの文字列", "iniファイルのPATH");
    // create and write ini file

    char buf[256];

    sprintf(buf, "%d", wait);
    WritePrivateProfileString(SECNAME, "wait", buf, filepath);

    sprintf(buf, "%d", speed);
    WritePrivateProfileString(SECNAME, "speed", buf, filepath);

    sprintf(buf, "%d", number);
    WritePrivateProfileString(SECNAME, "number", buf, filepath);

    sprintf(buf, "%d", fps_disp_enable);
    WritePrivateProfileString(SECNAME, "fps_disp_enable", buf, filepath);


iniファイルからの読み込みは、GetPrivateProfileString() を使って文字列として読み込むのが一般的らしいけど、今回は Int値を読み込めれば十分なので、Int値を返してくる GetPrivateProfileInt() を使って済ませることにした。この GetPrivateProfileInt()、もし、指定したキー名が無かったときは、デフォルト値を返すらしい。
int GetPrivateProfileInt("セクション名", "キー名", デフォルト値, "iniファイルPATH")
    // read key value from ini file

    wait = GetPrivateProfileInt(SECNAME, "wait", -1, filepath);
    speed = GetPrivateProfileInt(SECNAME, "speed", -1, filepath);
    number = GetPrivateProfileInt(SECNAME, "number", -1, filepath);
    fps_disp_enable = GetPrivateProfileInt(SECNAME, "fps_disp_enable", -1, filepath);

とりあえず、これで iniファイルの読み書きはできそう。

ただ、このプログラム、英数字のみのPATHにしか対応できない気もする…。PATHを格納するバッファを char で用意してるので、日本語文字列を含むディレクトリやファイル名には対応できないのではないかな…。いや、日本語文字列でフォルダを作ってその中で試したら、一応動いているようではあるけれど…。

2024/01/15(月) [n年前の日記]

#1 [prog] C/C++で設定ダイアログを表示してみたい

C/C++ と OpenGL を使ってWindows用のスクリーンセーバを作りたい。えてしてWindows用のスクリーンセーバは、設定ダイアログを表示して、動作を多少変更することができたりする。

そんなわけで、C/C++ を使って設定ダイアログを表示する方法について調べていた。
一般的なスクリーンセーバの場合、設定ダイアログ上でGUI部品(コントロール、ウィジェットとも呼ばれる)を配置するには、リソースファイル(拡張子が .rc)を使う。
_ダイアログ(Win32API)(C言語) - 超初心者向けプログラミング入門

また、設定ダイアログ上のGUI部品を操作するための関数も用意されていた。

一応、Visual Studio Community 2019 上で試したところ、それらしく動くプログラムを作れたのだけど…。これを MinGW (gcc 6.3.0) で再現しようとしたら、リソースコンパイラ windres.exe が syntax error を吐いて、なかなか上手く行かない…。windres.exe では、msctls_trackbar32 (trackbar、slider) は指定できないのだろうか…? ググっても情報が全く出てこないし…。

Visual Studio版 :

とりあえず、Visual Studio Community 2019上で動作したソースを載せておく。

どういう動作をするサンプルかというと…。
  • メインウインドウ上の "Open Dialog" ボタンを押すと、ダイアログが開く。(リソースファイルで定義したダイアログを開けている)
  • ダイアログ上で値を変更して "OK" ボタンを押すと、メインウインドウ上の値の表示も変わる。(ダイアログ上で設定した値を読み取れている。)

_dialogtest01.cpp
#include <Windows.h>
#include <wchar.h>
#include <stdlib.h>
#include <tchar.h>
#include <CommCtrl.h>

#include "framework.h"
#include "DialogTest01.h"

#define MAX_LOADSTRING 100

#define IDC_BUTTON_OPENDLG 110

// global work
HINSTANCE hInst;
WCHAR szTitle[MAX_LOADSTRING];                  // title bar text
WCHAR szWindowClass[MAX_LOADSTRING];            // main window class name

static WCHAR txt[1024];     // message work

// global paramater work
static int waitValue = 15;
static int speedValue = 1000;
static int numberValue = 1000;
static int fps_display = 1;

// prototype
LRESULT CALLBACK    WndProc(HWND, UINT, WPARAM, LPARAM);
INT_PTR CALLBACK    DlgProc(HWND, UINT, WPARAM, LPARAM);

// entry point
int APIENTRY wWinMain(_In_ HINSTANCE hInstance,
    _In_opt_ HINSTANCE hPrevInstance,
    _In_ LPWSTR    lpCmdLine,
    _In_ int       nCmdShow)
{
    UNREFERENCED_PARAMETER(hPrevInstance);
    UNREFERENCED_PARAMETER(lpCmdLine);

    // init global sstring
    LoadStringW(hInstance, IDS_APP_TITLE, szTitle, MAX_LOADSTRING);
    LoadStringW(hInstance, IDC_DIALOGTEST01, szWindowClass, MAX_LOADSTRING);

    // Register window class
    {
        WNDCLASSEXW wcex;

        wcex.cbSize = sizeof(WNDCLASSEX);

        wcex.style = CS_HREDRAW | CS_VREDRAW;
        wcex.lpfnWndProc = WndProc;
        wcex.cbClsExtra = 0;
        wcex.cbWndExtra = 0;
        wcex.hInstance = hInstance;
        wcex.hIcon = LoadIcon(hInstance, MAKEINTRESOURCE(IDI_DIALOGTEST01));
        wcex.hCursor = LoadCursor(nullptr, IDC_ARROW);
        wcex.hbrBackground = (HBRUSH)(COLOR_WINDOW + 1);

        wcex.lpszMenuName = nullptr;        // set menu

        wcex.lpszClassName = szWindowClass;
        wcex.hIconSm = LoadIcon(wcex.hInstance, MAKEINTRESOURCE(IDI_SMALL));

        RegisterClassExW(&wcex);
    }

    // init application. create main window
    {
        hInst = hInstance;

        HWND hWnd = CreateWindowW(
            szWindowClass,
            szTitle,
            WS_OVERLAPPEDWINDOW,
            CW_USEDEFAULT, CW_USEDEFAULT,   // x, y
            320, 240,                       // width, height
            nullptr,
            nullptr,
            hInstance,
            nullptr
        );

        if (!hWnd) return FALSE;

        ShowWindow(hWnd, nCmdShow);     // display main window
        UpdateWindow(hWnd);
    }

    MSG msg;

    // main message loop
    while (GetMessage(&msg, nullptr, 0, 0))
    {
        TranslateMessage(&msg);
        DispatchMessage(&msg);
    }

    return (int)msg.wParam;
}

// main window procedure
LRESULT CALLBACK WndProc(HWND hWnd, UINT message, WPARAM wParam, LPARAM lParam)
{
    // static WCHAR txt[1024];

    switch (message)
    {
    case WM_CREATE:
        // create main window

        // create "Open Dialog" button
        CreateWindow(
            L"BUTTON", L"Open Dialog",
            WS_CHILD | WS_VISIBLE,
            20, 20, 100, 24,        // x, y, width, height
            hWnd, (HMENU)IDC_BUTTON_OPENDLG, hInst, NULL
        );
        break;

    case WM_COMMAND:
    {
        int wmId = LOWORD(wParam);
        switch (wmId)
        {
        case IDC_BUTTON_OPENDLG:
        {
            // push "Open Dialog" button

            // Open dialog
            int ret;
            ret = DialogBox(hInst, MAKEINTRESOURCE(IDD_DIALOG1), hWnd, DlgProc);

            // dialog results
            switch (ret)
            {
            case IDOK:
                // push "OK" button
                wcscpy_s(txt, L"IDOK");         // set text message
                break;

            case IDCANCEL:
                // push "Cancel" button
                wcscpy_s(txt, L"IDCANCEL");     // set text message
                break;
            }
            InvalidateRect(hWnd, NULL, TRUE);
        }
        break;

        default:
            return DefWindowProc(hWnd, message, wParam, lParam);
        }
    }
    break;

    case WM_PAINT:
    {
        // draw main window

        PAINTSTRUCT ps;
        HDC hdc = BeginPaint(hWnd, &ps);

        // draw message
        TextOut(hdc, 20, 70, txt, lstrlen(txt));

        // draw paramater value
        {
            WCHAR s[256];

            swprintf_s(s, L"wait=%d", waitValue);
            TextOut(hdc, 20, 90, s, lstrlen(s));

            swprintf_s(s, L"speed=%d", speedValue);
            TextOut(hdc, 20, 110, s, lstrlen(s));

            swprintf_s(s, L"number=%d", numberValue);
            TextOut(hdc, 20, 130, s, lstrlen(s));

            swprintf_s(s, L"fps_display=%d", fps_display);
            TextOut(hdc, 20, 150, s, lstrlen(s));
        }

        EndPaint(hWnd, &ps);
    }
    break;

    case WM_DESTROY:
        // destroy main window
        PostQuitMessage(0);
        break;

    default:
        return DefWindowProc(hWnd, message, wParam, lParam);
    }
    return 0;
}

// dialog procedure
INT_PTR CALLBACK DlgProc(HWND hDlg, UINT message, WPARAM wParam, LPARAM lParam)
{
    switch (message) {
    case WM_INITDIALOG:
    {
        // initialize dialog
        HWND cHwnd;

        // wait slider (trackbar)
        cHwnd = GetDlgItem(hDlg, IDC_SLIDER1);      // get hundle
        SendMessage(cHwnd, TBM_SETRANGE, TRUE, MAKELPARAM(5, 200)); // set range
        SendMessage(cHwnd, TBM_SETTICFREQ, 5, 0);                   // set tick
        SendMessage(cHwnd, TBM_SETPOS, TRUE, (LPARAM)waitValue);    // set position

        // speed slider
        cHwnd = GetDlgItem(hDlg, IDC_SLIDER2);
        SendMessage(cHwnd, TBM_SETRANGE, TRUE, MAKELPARAM(100, 4000));
        SendMessage(cHwnd, TBM_SETTICFREQ, 100, 0);
        SendMessage(cHwnd, TBM_SETPOS, TRUE, (LPARAM)speedValue);

        // number slider
        cHwnd = GetDlgItem(hDlg, IDC_SLIDER3);
        SendMessage(cHwnd, TBM_SETRANGE, TRUE, MAKELPARAM(10, 4000));
        SendMessage(cHwnd, TBM_SETTICFREQ, 100, 0);
        SendMessage(cHwnd, TBM_SETPOS, TRUE, (LPARAM)numberValue);

        // init checkbox
        SendMessage(GetDlgItem(hDlg, IDC_CHECK1), BM_SETCHECK,
            ((fps_display == 0) ? BST_UNCHECKED : BST_CHECKED), 0);
    }
    return (INT_PTR)TRUE;

    case WM_COMMAND:
        switch (LOWORD(wParam))
        {
        case IDOK:
            // push "OK" button
            // get paramater and set global work

            waitValue = SendMessage(GetDlgItem(hDlg, IDC_SLIDER1), TBM_GETPOS, NULL, NULL);
            speedValue = SendMessage(GetDlgItem(hDlg, IDC_SLIDER2), TBM_GETPOS, NULL, NULL);
            numberValue = SendMessage(GetDlgItem(hDlg, IDC_SLIDER3), TBM_GETPOS, NULL, NULL);

            if (SendMessage(GetDlgItem(hDlg, IDC_CHECK1), BM_GETCHECK, 0, 0) == BST_UNCHECKED)
                fps_display = 0;
            else
                fps_display = 1;

            EndDialog(hDlg, IDOK);
            return (INT_PTR)TRUE;

        case IDCANCEL:
            // push "Cancel" button
            EndDialog(hDlg, IDCANCEL);
            return (INT_PTR)TRUE;

        case IDC_BUTTON_RESET:
        {
            // push "Reset" button

            // change status text
            SetWindowText(GetDlgItem(hDlg, IDC_STATUS1), TEXT("Reset"));

            // set slider position
            SendMessage(GetDlgItem(hDlg, IDC_SLIDER1), TBM_SETPOS, TRUE, 15);
            SendMessage(GetDlgItem(hDlg, IDC_SLIDER2), TBM_SETPOS, TRUE, 1000);
            SendMessage(GetDlgItem(hDlg, IDC_SLIDER3), TBM_SETPOS, TRUE, 1000);

            // change checkbox
            SendMessage(GetDlgItem(hDlg, IDC_CHECK1), BM_SETCHECK, BST_CHECKED, 0);
        }
        return (INT_PTR)TRUE;
        }
        break;
    }
    return (INT_PTR)FALSE;
}

_resource.h
//{{NO_DEPENDENCIES}}
// Microsoft Visual C++ で生成されたインクルード ファイル。
// DialogTest01.rc で使用
//
#define IDC_MYICON                      2
#define IDD_DIALOGTEST01_DIALOG         102
#define IDS_APP_TITLE                   103
#define IDI_DIALOGTEST01                104
#define IDI_SMALL                       105
#define IDC_DIALOGTEST01                106
#define IDR_MAINFRAME                   128
#define IDD_DIALOG1                     129
#define IDC_CHECK1                      1000
#define IDC_BUTTON_RESET                1001
#define IDC_SLIDER1                     1002
#define IDC_SLIDER2                     1003
#define IDC_SLIDER3                     1004
#define IDC_STATUS1                     1005
#define IDC_STATIC                      -1

// Next default values for new objects
// 
#ifdef APSTUDIO_INVOKED
#ifndef APSTUDIO_READONLY_SYMBOLS
#define _APS_NO_MFC                     1
#define _APS_NEXT_RESOURCE_VALUE        130
#define _APS_NEXT_COMMAND_VALUE         32771
#define _APS_NEXT_CONTROL_VALUE         1007
#define _APS_NEXT_SYMED_VALUE           110
#endif
#endif

_dialogtest01.rc
// Microsoft Visual C++ generated resource script.
//
#include "resource.h"

#define APSTUDIO_READONLY_SYMBOLS
/////////////////////////////////////////////////////////////////////////////
//
// Generated from the TEXTINCLUDE 2 resource.
//
#ifndef APSTUDIO_INVOKED
#include "targetver.h"
#endif
#define APSTUDIO_HIDDEN_SYMBOLS
#include "windows.h"
#undef APSTUDIO_HIDDEN_SYMBOLS

/////////////////////////////////////////////////////////////////////////////
#undef APSTUDIO_READONLY_SYMBOLS

/////////////////////////////////////////////////////////////////////////////
// 日本語 (日本) resources

#if !defined(AFX_RESOURCE_DLL) || defined(AFX_TARG_JPN)
LANGUAGE LANG_JAPANESE, SUBLANG_DEFAULT

/////////////////////////////////////////////////////////////////////////////
//
// Icon
//

// Icon with lowest ID value placed first to ensure application icon
// remains consistent on all systems.
IDI_DIALOGTEST01        ICON                    "DialogTest01.ico"

IDI_SMALL               ICON                    "small.ico"


/////////////////////////////////////////////////////////////////////////////
//
// Dialog
//

IDD_DIALOG1 DIALOGEX 0, 0, 289, 140
STYLE DS_SETFONT | DS_MODALFRAME | WS_POPUP | WS_CAPTION | WS_SYSMENU
CAPTION "Dialog"
 FONT 11, "Segoe UI", 400, 0, 0x0
BEGIN
    DEFPUSHBUTTON   "&OK",IDOK,168,96,50,14
    PUSHBUTTON      "&Cancel",IDCANCEL,228,96,50,14
    PUSHBUTTON      "Reset",IDC_BUTTON_RESET,84,96,50,14
    CONTROL         "",IDC_SLIDER1,"msctls_trackbar32",TBS_AUTOTICKS | TBS_BOTH | TBS_TOOLTIPS | WS_TABSTOP,96,24,180,15
    CONTROL         "",IDC_SLIDER2,"msctls_trackbar32",TBS_AUTOTICKS | TBS_BOTH | TBS_TOOLTIPS | WS_TABSTOP,96,48,180,15
    CONTROL         "",IDC_SLIDER3,"msctls_trackbar32",TBS_AUTOTICKS | TBS_BOTH | TBS_TOOLTIPS | WS_TABSTOP,96,72,180,15
    CONTROL         "FPS display",IDC_CHECK1,"Button",BS_AUTOCHECKBOX | WS_TABSTOP,12,96,53,10
    CTEXT           "ssstars GL setting",IDC_STATIC,7,7,275,8
    LTEXT           "Wait (5 - 200msec)",IDC_STATIC,12,24,63,8
    LTEXT           "Speed (100 - 4000)",IDC_STATIC,12,48,64,8
    LTEXT           "Number (10 - 4000)",IDC_STATIC,12,72,66,8
    LTEXT           "Status",IDC_STATUS1,7,120,275,13,SS_SUNKEN | WS_BORDER
END


/////////////////////////////////////////////////////////////////////////////
//
// DESIGNINFO
//

#ifdef APSTUDIO_INVOKED
GUIDELINES DESIGNINFO
BEGIN
    IDD_ABOUTBOX, DIALOG
    BEGIN
        LEFTMARGIN, 7
        RIGHTMARGIN, 162
        TOPMARGIN, 7
        BOTTOMMARGIN, 55
    END

    IDD_DIALOG1, DIALOG
    BEGIN
        LEFTMARGIN, 7
        RIGHTMARGIN, 282
        TOPMARGIN, 7
        BOTTOMMARGIN, 133
    END
END
#endif    // APSTUDIO_INVOKED


#ifdef APSTUDIO_INVOKED
/////////////////////////////////////////////////////////////////////////////
//
// TEXTINCLUDE
//

1 TEXTINCLUDE 
BEGIN
    "resource.h\0"
END

2 TEXTINCLUDE 
BEGIN
    "#ifndef APSTUDIO_INVOKED\r\n"
    "#include ""targetver.h""\r\n"
    "#endif\r\n"
    "#define APSTUDIO_HIDDEN_SYMBOLS\r\n"
    "#include ""windows.h""\r\n"
    "#undef APSTUDIO_HIDDEN_SYMBOLS\r\n"
    "\0"
END

3 TEXTINCLUDE 
BEGIN
    "\r\n"
    "\0"
END

#endif    // APSTUDIO_INVOKED


/////////////////////////////////////////////////////////////////////////////
//
// String Table
//

STRINGTABLE
BEGIN
    IDS_APP_TITLE           "DialogTest01"
    IDC_DIALOGTEST01        "DIALOGTEST01"
END

#endif    // 日本語 (日本) resources
/////////////////////////////////////////////////////////////////////////////



#ifndef APSTUDIO_INVOKED
/////////////////////////////////////////////////////////////////////////////
//
// Generated from the TEXTINCLUDE 3 resource.
//


/////////////////////////////////////////////////////////////////////////////
#endif    // not APSTUDIO_INVOKED

_dialogtest01.h
_framework.h

ビルドして実行すると以下のような見た目になる。

ss_dialogtest01.png

分かったことをメモ :

試してみて分かったことをメモ。

リソースファイルの記述内容に基づいてダイアログを表示するには、DialogBox() を使う。

_第13章 ダイアログボックス
_DialogBox関数でリソースからモーダルダイアログを作る | ぬの部屋(仮)

DialogBox() には、リソースファイル内でダイアログ関係を定義してある部分のID(今回のソースなら、IDD_DIALOG1)と、ダイアログプロシージャを指定して呼び出す。これだけでダイアログが表示される。

ダイアログプロシージャ(今回は DlgProc() が担当)は、ダイアログ上でユーザが何か操作をしたときに呼ばれる関数、らしい。たぶん。

ダイアログプロシージャが呼ばれた際は、ダイアログが今どんな状態なのかをメッセージとして送ってくる。
  • WM_INITDIALOG ... ダイアログが表示された際に一度だけ呼ばれる。ダイアログ上の部品の値等を初期化したい時はここで行う。
  • WM_COMMAND ... ボタン等が押された時に呼ばれる。一緒に送られてきたIDを見て、どのボタンが押されたのかを判別して処理を書ける。

ダイアログ上のGUI部品の値を設定したり、値を読み取ったりするときは、SendMessage() その他を使う。get したいのか、set したいのかを指定することで、値を読んだり、値を変更したりできる。また、SendMessage() 以外にも、特定の種類のGUI部品に対して簡単に操作できる関数が用意されていたりもする。(EDITTEXT に対する GetDlgItemText(), SetDlgItemText() とか。)

SendMessage() には、どのGUI部品を対象にしたいのか、ハンドルなるものを渡して教えてやらないといけない。 GUI部品のハンドルは、GetDlgItem() で取得できる。

_リソースから作成したダイアログボックスのコントロールへアクセスする | ぬの部屋(仮)

ダイアログ上にtrackbar(slide) を出したい時は、"msctls_trackbar32" を指定してやる。コレを使う時は、.c/.cpp 内で、commctrl.h を include してやる必要がある。そうしないと、TBM_* というシンボルが使えない。

_トラックバー - インコのWindowsSDK

MinGW版でハマってる :

ということで、Visual Studio上では動いてくれたけど、MinGW (gcc 6.3.0) では上手く行かない…。リソースコンパイラ windres.exe が syntax error を出す。

渡してる resource.rc は以下。

/* resource */

#include <windows.h>
#include <commctrl.h>
#include "resource.h"

/* Icon */
IDI_APP_ICON    ICON    "appli.ico"
IDI_SMALL       ICON    "small.ico"

/* Dialog */
IDD_DIALOG1 DIALOG DISCARDABLE  0, 0, 289, 140
STYLE DS_MODALFRAME | WS_POPUP | WS_VISIBLE | WS_CAPTION | WS_SYSMENU
CAPTION "Dialog"
 FONT 11, "Segoe UI"
BEGIN
  DEFPUSHBUTTON "OK",      IDOK,      168, 96, 50, 14
  PUSHBUTTON    "Cancel",  IDCANCEL,  228, 96, 50, 14
  PUSHBUTTON    "Reset",   IDC_RESET, 10, 100, 50, 14
  AUTOCHECKBOX  "FPS display", IDC_FPSDISPLAY, 12, 96, 53, 10
  CTEXT "ssstars GL setting", IDC_STATIC, 7,7,275,8
  LTEXT "Wait (5 - 200msec)", IDC_STATIC, 12,24,63,8
  LTEXT "Speed (100 - 4000)", IDC_STATIC, 12,48,64,8
  LTEXT "Number (10 - 4000)", IDC_STATIC, 12,72,66,8
  LTEXT "Status", IDC_STATUS1, 7,120,275,13,SS_SUNKEN | WS_BORDER
  CONTROL "Wait",   IDC_WAITVALUE, "msctls_trackbar32", TBS_AUTOTICKS | TBS_BOTH | TBS_TOOLTIPS | WS_TABSTOP, 96,24,180,15
  CONTROL "Speed",  IDC_SPEED,     "msctls_trackbar32", TBS_AUTOTICKS | TBS_BOTH | TBS_TOOLTIPS | WS_TABSTOP, 96,48,180,15
  CONTROL "Number", IDC_NUMBER,    "msctls_trackbar32", TBS_AUTOTICKS | TBS_BOTH | TBS_TOOLTIPS | WS_TABSTOP, 96,72,180,15
END

以下のあたりでエラーを出している。これは trackbar(slider?) を定義してる。"msctls_trackbar32" というのが、trackbar のはずだけど…。
  CONTROL "Wait",   IDC_WAITVALUE, "msctls_trackbar32", TBS_AUTOTICKS | TBS_BOTH | TBS_TOOLTIPS | WS_TABSTOP, 96,24,180,15
  CONTROL "Speed",  IDC_SPEED,     "msctls_trackbar32", TBS_AUTOTICKS | TBS_BOTH | TBS_TOOLTIPS | WS_TABSTOP, 96,48,180,15
  CONTROL "Number", IDC_NUMBER,    "msctls_trackbar32", TBS_AUTOTICKS | TBS_BOTH | TBS_TOOLTIPS | WS_TABSTOP, 96,72,180,15

もしかすると、windres.exe は、このコントロール(GUI部品、ウィジェット)に未対応だったりするのだろうか…? 代わりに EDITTEXT を記述すると、あっさりビルドが通るし…。

2024/01/16(火) [n年前の日記]

#1 [prog] C/C++で設定ダイアログを表示してみたいその2

C/C++ と OpenGL を使ってWindows用のスクリーンセーバを作りたい。そのためには設定ダイアログを表示して、設定値の読み書きしないといけない。

_昨日の実験 で、Windows10 x64 22H2 + Visual Studio Community 2019上なら、求める動作を書けたのだけど。同じことを MinGW (gcc 6.3.0) で再現しようとしたところ躓いてしまった。

リソースコンパイラ windres 2.28 が resource.rc をコンパイルできない…。trackbar/sliderというか、要は "msctls_trackbar32" というコントロール(GUI部品)の記述行で syntax error が出てしまって…。
  CONTROL "Wait",   IDC_WAITVALUE, "msctls_trackbar32", TBS_AUTOTICKS | TBS_BOTH | TBS_TOOLTIPS | WS_TABSTOP, 96,24,180,15
  CONTROL "Speed",  IDC_SPEED,     "msctls_trackbar32", TBS_AUTOTICKS | TBS_BOTH | TBS_TOOLTIPS | WS_TABSTOP, 96,48,180,15
  CONTROL "Number", IDC_NUMBER,    "msctls_trackbar32", TBS_AUTOTICKS | TBS_BOTH | TBS_TOOLTIPS | WS_TABSTOP, 96,72,180,15

結局、問題を解決することはできなかった。諦めて該当行を EDITTEXT に置き換えて処理するようにしてしまった。

環境は、Windows10 x64 22H2 + MinGW (gcc 6.3.0)。

ソース :

ひとまず MinGW (gcc 6.3.0) でも動くようになったサンプルを載せてみる。
  • メインウインドウのボタンを押すとリソースファイルで記述した設定ダイアログが表示される → DialogBox() を使う。
  • 設定ダイアログに、現在の設定値を反映 → GetDlgItem()、SendMessage()、SetDlgItemText() を使う。
  • 設定ダイアログ上で入力欄に数値を入力してOKボタンを押すと、メインウインドウ上でその設定値を読み取れる → GetDlgItem()、SendMessage()、GetDlgItemText()、IsDlgButtonChecked() を使う。

今回はソース内に日本語コメントをガンガン書いてみた。

_01_dialogtest.c
// DialogTest01.c
//
// Dialog access sample
//
// Last updated: <2024/01/16 22:17:34 +0900>

#include <tchar.h>
#include <wchar.h>
#include <windows.h>
#include <stdio.h>
#include <stdlib.h>

// もし trackbar を使う時は commctrl.h の include が必要
// #include <commctrl.h>

// リソースファイルを使う時は include する
#include "resource.h"

// メインウインドウ上に設置するボタンのID
#define IDC_BUTTON_OPENDLG 110

// global work グローバル変数
HINSTANCE hInst;

// static WCHAR txt[1024]; // message work
static char txt[1024]; // message work

// global paramater work
static int waitValue = 15;
static int speedValue = 1000;
static int numberValue = 1000;
static int fps_display = 1;

// ========================================
// prototype プロトタイプ宣言
LRESULT CALLBACK WndProc(HWND, UINT, WPARAM, LPARAM);
INT_PTR CALLBACK DlgProc(HWND, UINT, WPARAM, LPARAM);

// ========================================
// entry point エントリーポイント。Win32 GUIアプリは最初にここが呼ばれる
int WINAPI WinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance,
                   LPSTR lpCmdLine, int nCmdShow)
{
  TCHAR szWindowClass[] = TEXT("DIALOGTEST01"); // main window class name メインウインドウクラス名
  TCHAR szTitle[] = TEXT("DialogTest01");       // title bar text タイトルバーに表示するテキスト
  WNDCLASSEX wc;
  MSG msg;

  // set window class properties (struct)
  // ウインドウクラス構造体の設定
  {
    wc.cbSize = sizeof(wc);
    wc.style = CS_HREDRAW | CS_VREDRAW;
    wc.lpfnWndProc = WndProc; // ウインドウプロシージャを指定
    wc.cbClsExtra = 0;
    wc.cbWndExtra = 0;
    wc.hInstance = hInstance;
    wc.hCursor = LoadCursor(NULL, IDC_ARROW);
    wc.hbrBackground = (HBRUSH)(COLOR_WINDOW + 1);
    wc.lpszMenuName = NULL;
    wc.lpszClassName = szWindowClass;
    wc.hIcon = NULL; // 今回はアイコンを指定しない
    wc.hIconSm = NULL;
    // wc.hIcon = LoadIcon(hInstance, MAKEINTRESOURCE(IDI_APP_ICON));
    // wc.hIconSm = LoadIcon(wc.hInstance, MAKEINTRESOURCE(IDI_SMALL));

    if (RegisterClassEx(&wc) == 0)
      return FALSE;
  }

  // create window ウインドウを作成
  {
    hInst = hInstance;

    HWND hWnd = CreateWindow(
        szWindowClass,
        szTitle, // タイトルバーのテキストを指定
        WS_OVERLAPPEDWINDOW,
        CW_USEDEFAULT, CW_USEDEFAULT, // x, y
        512, 288,                     // width, height
        NULL, NULL, hInstance, NULL);

    if (hWnd == NULL)
      return FALSE;

    // display window ウインドウを表示
    ShowWindow(hWnd, nCmdShow);
    UpdateWindow(hWnd);
  }

  // main message loop メッセージループ
  {
    BOOL bRet;
    while ((bRet = GetMessage(&msg, NULL, 0, 0)) != 0)
    {
      if (bRet == -1)
      {
        break;
      }
      else
      {
        TranslateMessage(&msg);
        DispatchMessage(&msg);
      }
    }
  }

  return (int)msg.wParam;
}

// ========================================
// main window procedure
// メインウインドウプロシージャ。メインウインドウ上で何かが起きるとここが呼ばれる
LRESULT CALLBACK WndProc(HWND hWnd, UINT message, WPARAM wParam, LPARAM lParam)
{
  // static WCHAR txt[1024];

  switch (message)
  {
  case WM_CREATE:
    // create main window. initialize
    // メインウインドウ作成時にここに来る。初期化処理をする

    // create "Open Dialog" button ボタンを1つ設置する
    CreateWindow(
        "BUTTON", "Open Dialog", // クラス名、ボタン上に表示するテキスト
        WS_CHILD | WS_VISIBLE,   // スタイル
        20, 20, 100, 24,         // x, y, width, height
        hWnd,
        (HMENU)IDC_BUTTON_OPENDLG, // ボタンに紐づけるID
        hInst,
        NULL);
    break;

  case WM_COMMAND:
  {
    // application menu process

    int wmId = LOWORD(wParam);
    switch (wmId)
    {
    case IDC_BUTTON_OPENDLG:
    {
      // push "Open Dialog" button
      // "Open Dialog" ボタンが押されるとここに来る

      // Open dialog リソースファイル内で記述されてるダイアログを開く
      int ret;
      ret = DialogBox(hInst, MAKEINTRESOURCE(IDD_DIALOG1), hWnd, DlgProc);

      // モーダルダイアログを開いているので、OK か Cancel が押されるまで処理は返ってこない

      // dialog results ダイアログが返してきた値で処理を分ける
      switch (ret)
      {
      case IDOK:
        // push "OK" button OKボタンが押された
        strcpy(txt, "IDOK");
        // wcscpy_s(txt, TEXT("IDOK"));
        break;

      case IDCANCEL:
        // push "Cancel" button Cancelボタンが押された
        strcpy(txt, "IDCANCEL");
        // wcscpy_s(txt, TEXT("IDCANCEL"));
        break;
      }
      InvalidateRect(hWnd, NULL, TRUE);
    }
    break;

    default:
      return DefWindowProc(hWnd, message, wParam, lParam);
    }
  }
  break;

  case WM_PAINT:
  {
    // draw main window メインウインドウ描画処理

    PAINTSTRUCT ps;
    HDC hdc = BeginPaint(hWnd, &ps);

    // draw message "IDOK" or "IDCANCEL" のメッセージを表示
    // TextOut(hdc, 20, 70, txt, lstrlen(txt));
    TextOut(hdc, 20, 70, txt, strlen(txt));

    // draw paramater value ダイアログ上で設定した値を表示
    {
      char s[256];

      sprintf(s, "wait=%d", waitValue);
      TextOut(hdc, 20, 90, s, strlen(s));

      sprintf(s, "speed=%d", speedValue);
      TextOut(hdc, 20, 110, s, strlen(s));

      sprintf(s, "number=%d", numberValue);
      TextOut(hdc, 20, 130, s, strlen(s));

      sprintf(s, "fps_display=%d", fps_display);
      TextOut(hdc, 20, 150, s, strlen(s));
    }

    EndPaint(hWnd, &ps);
  }
  break;

  case WM_DESTROY:
    // destroy window ウインドウ破棄時の処理
    PostQuitMessage(0);
    break;

  default:
    return DefWindowProc(hWnd, message, wParam, lParam);
  }
  return 0;
}

// 与えられた値を最小値と最大値の間に収める
int clamp(int v, int minv, int maxv)
{
  if (v < minv)
    return minv;
  if (v > maxv)
    return maxv;
  return v;
}

// set value to config dialog
// 設定ダイアログの値を設定する
void SetvalueToDialog(HWND hDlg, int waitValue, int speedValue, int numberValue, int fps_display)
{
  // set edittext
  // EDITTEXTの内容を初期化
  // SetDlgItemText()で EDITTEXTの内容(文字列)を変更できる
  {
    char s[256];

    sprintf(s, "%d", waitValue);
    SetDlgItemText(hDlg, IDC_WAITVALUE, s);

    sprintf(s, "%d", speedValue);
    SetDlgItemText(hDlg, IDC_SPEED, s);

    sprintf(s, "%d", numberValue);
    SetDlgItemText(hDlg, IDC_NUMBER, s);
  }

  // set checkbox
  // CHECKBOXの状態を初期化する
  // GetDlgItem() でコントロールのハンドルを取得できる
  // SendMessage() でコントロールの状態を変更できる
  // BM_SETCHECK で状態の変更を指示
  {
    HWND cHwnd;
    cHwnd = GetDlgItem(hDlg, IDC_FPSDISPLAY);
    SendMessage(cHwnd, BM_SETCHECK, ((fps_display == 0) ? BST_UNCHECKED : BST_CHECKED), 0);
  }
}

// get value from confgi dialog
void GetValueFromDialog(HWND hDlg)
{
  // get edittext text
  // EDITTEXTの内容(文字列)をint値にしてグローバル変数に記録
  // GetDlgItemText() でEDITTEXTの内容(文字列)を取得
  // atoi() で文字列をint値に変換
  {
    char s[1024];

    GetDlgItemText(hDlg, IDC_WAITVALUE, s, sizeof(s));
    waitValue = clamp(atoi(s), 5, 200);

    GetDlgItemText(hDlg, IDC_SPEED, s, sizeof(s));
    speedValue = clamp(atoi(s), 100, 4000);

    GetDlgItemText(hDlg, IDC_NUMBER, s, sizeof(s));
    numberValue = clamp(atoi(s), 10, 4000);
  }

  // get checkbox status
  // CHECKBOXの状態をグローバル変数に記録
  // IsDlgButtonChecked() で CHECKBOX の状態を取得できる
  fps_display = (IsDlgButtonChecked(hDlg, IDC_FPSDISPLAY) == BST_CHECKED) ? 1 : 0;
}

// ========================================
// dialog procedure
// ダイアログプロシージャ。ダイアログ上でボタン等が押された際に呼ばれる
INT_PTR CALLBACK DlgProc(HWND hDlg, UINT message, WPARAM wParam, LPARAM lParam)
{
  switch (message)
  {
  case WM_INITDIALOG:
  {
    // initialize dialog
    // ダイアログ初期化時の処理。GUI部品の状態を設定

    // 設定ダイアログ上の値を設定
    SetvalueToDialog(hDlg, waitValue, speedValue, numberValue, fps_display);
  }
    return TRUE;

  case WM_COMMAND:
    switch (LOWORD(wParam))
    {
    case IDOK:
      // push "OK" button "OK"ボタンが押されたらここに来る
      GetValueFromDialog(hDlg); // 設定ダイアログ上の値を取得してグローバル変数に記録
      EndDialog(hDlg, IDOK);    // ダイアログを終了
      return TRUE;

    case IDCANCEL:
      // push "Cancel" button
      EndDialog(hDlg, IDCANCEL); // ダイアログを終了
      return TRUE;

    case IDC_RESET:
    {
      // push "Reset" button "Reset"ボタンが押された

      // change status text
      // ダイアログ上のLABELのテキストを変更
      SetWindowText(GetDlgItem(hDlg, IDC_STATUS1), TEXT("Reset"));

      // ダイアログ上の値をデフォルト値に変更
      SetvalueToDialog(hDlg, 15, 1000, 1000, 1);
    }
      return TRUE;
    }
    break;
  }
  return FALSE;
}

_resource.h
#ifndef RESOURCE_H_
#define RESOURCE_H_

#define IDC_MYICON                      2
#define IDD_DIALOGTEST01_DIALOG         102
#define IDI_APP_ICON                    110
#define IDI_SMALL                       111
#define IDR_MAINFRAME                   128
#define IDD_DIALOG1                     129
#define IDC_FPSDISPLAY                  2003
#define IDC_RESET                       2004
#define IDC_WAITVALUE                   2005
#define IDC_SPEED                       2006
#define IDC_NUMBER                      2007
#define IDC_STATUS1                     2008
#define IDC_STATIC                      -1

#endif

_resource.rc
/* resource */

#include <windows.h>
/* #include <commctrl.h> */
#include "resource.h"

/* Dialog */
IDD_DIALOG1 DIALOG DISCARDABLE  0, 0, 289, 140
STYLE DS_MODALFRAME | WS_POPUP | WS_VISIBLE | WS_CAPTION | WS_SYSMENU
CAPTION "Setting"
 FONT 11, "Segoe UI"
BEGIN
  DEFPUSHBUTTON "OK",      IDOK,      172, 96, 50, 14
  PUSHBUTTON    "Cancel",  IDCANCEL,  228, 96, 50, 14
  CTEXT "ssstars GL setting", IDC_STATIC, 7, 7, 275, 8
  LTEXT "Wait (5 - 200 msec)", IDC_STATIC, 12, 24, 72, 8
  LTEXT "Speed (100 - 4000)", IDC_STATIC, 12, 48, 72, 8
  LTEXT "Number (10 - 4000)", IDC_STATIC, 12, 72, 72, 8
  LTEXT "Status", IDC_STATUS1, 7,120,275,13,SS_SUNKEN | WS_BORDER
  EDITTEXT IDC_WAITVALUE, 96, 24, 100, 16, ES_AUTOHSCROLL
  EDITTEXT IDC_SPEED,     96, 48, 100, 16, ES_AUTOHSCROLL
  EDITTEXT IDC_NUMBER,    96, 72, 100, 16, ES_AUTOHSCROLL
  AUTOCHECKBOX  "FPS display", IDC_FPSDISPLAY, 12, 96, 53, 10
  PUSHBUTTON    "Reset",   IDC_RESET, 76,  96, 50, 14
END

_Makefile
PROGRAM=01_dialogtest.exe

SRC=$(PROGRAM:.exe=.c)
OBJS=$(SRC:.c=.o) resource.o

.PHONY: all
all: $(PROGRAM)

$(PROGRAM): $(OBJS) Makefile
    gcc $(OBJS) -mwindows -o $@

%.o: %.c
    gcc -O3 -o $@ -c $<

resource.o: resource.rc Makefile
    windres $< -o $@

.PHONY: cleanall clean

cleanall: clean
    rm -f $(PROGRAM)

.PHONY: clean
clean:
    rm -f $(OBJS)

ビルドと実行結果 :

make と打ってビルドすると、01_dialogtest.exe が得られる。実行結果は以下のような感じ。設定ダイアログ上で設定した値を、メインウインドウ側で読めていることが分かる。

課題 :

ひとまず、MinGW を使っても、この手の処理を書くことができた。一部のコントロール(GU部品、ウィジェット)を使えない点がなんだか気になるけれど…。単に windres 2.28 (windres.exe) がサポートしてないのか、それとも書き方がおかしいのか。どちらが原因なのかは不明。

後は、先日実験した iniファイルの読み書き処理を追加すれば、スクリーンセーバの設定を設定ダイアログで変更、かつ、iniファイルに保存、といった処理も作れるのではないか、と…。

_mieki256's diary - C言語でiniファイルの読み書きをしたい

2024/01/17追記 :

MinGW + windres でも trackbar をリソースファイル内に記述する方法が分かった。 _2024/01/17の日記 にメモしておいた。

#2 [anime] 「勇気爆発バーンブレイバーン」1話を視聴

録画していたので視聴。日本のロボットアニメの作画面に関しては第一人者とされている大張正己監督の作品。ロボットアニメ。制作はCygamesPictures。

Web上の感想を眺めていたら1話の最後のあたりでどうも皆さん衝撃を受けていたようで、これは一体何を見せられるのだろうとドキドキしながら視聴してみたら…。参りました。これはたしかに衝撃を受けるわ…。いやはや、そう来るか…。そうなっちゃうのか…。素晴らしい。

冒頭のバトルシーンを見て、ロボットのデザインに違和感があったのだよな…。大張監督の作風なら、こんな武骨(?)で特徴のない(?)なデザインはアリにしない印象だけど、もしや今作は今までと違う作風を模索していたりするのだろうか、などと想像していたら見事にやられた。参りました。

2024/01/17(水) [n年前の日記]

#1 [prog] C/C++で設定ダイアログを表示してみたいその3

C/C++ と OpenGL を使ってWindows用のスクリーンセーバを作りたい。そのためには設定ダイアログを表示して、設定値の読み書きしないといけない。環境は Windows10 x64 22H2 + MnGW (gcc 6.3.0)。

_昨日の実験 では、リソースファイル(resource.rc)内のダイアログ設定部分に trackbar (トラックバー/スライダー/TRACKBAR_CLASS/"msctls_trackbar32") を記述しても、MinGW の windres 2.28 では syntax error になってしまってリソースをコンパイルすることができなかったので、諦めて EDITTEXT に置き換えて誤魔化していたけれど。

  CONTROL "Wait",   IDC_WAITVALUE, "msctls_trackbar32", TBS_AUTOTICKS | TBS_TOOLTIPS | WS_TABSTOP, 96,24,180,15
  CONTROL "Speed",  IDC_SPEED,     "msctls_trackbar32", TBS_AUTOTICKS | TBS_TOOLTIPS | WS_TABSTOP, 96,48,180,15
  CONTROL "Number", IDC_NUMBER,    "msctls_trackbar32", TBS_AUTOTICKS | TBS_TOOLTIPS | WS_TABSTOP, 96,72,180,15

↓

  EDITTEXT IDC_WAITVALUE, 96, 24, 100, 16, ES_AUTOHSCROLL
  EDITTEXT IDC_SPEED,     96, 48, 100, 16, ES_AUTOHSCROLL
  EDITTEXT IDC_NUMBER,    96, 72, 100, 16, ES_AUTOHSCROLL

その後色々試していたら、trackbar/sliderを記述してもエラーにならない書き方が分かった。

原因と対策 :

エラーの原因は、trackbar を記述する際、スタイルとして指定していた TBS_TOOLTIPS が未定義になっていたせいだった。

TBS_TOOLTIPS は commctrl.h の中で定義されているので、本来なら resource.rc の最初のほうで、「#include <commctrl.h>」 を書いておけばそれだけで TBS_TOOLTIPS も使えるようになるはずだけど。commctrl.h を眺めてみたところ、以下のような記述になっていた。
#if (_WIN32_IE >= 0x0300)
#define TBS_TOOLTIPS 0x0100
#define TBTS_TOP 0
#define TBTS_LEFT 1
#define TBTS_BOTTOM 2
#define TBTS_RIGHT 3
#endif

「_WIN32_IE」が 0x0300 以上の値になってないと、TBS_TOOLTIPS は未定義になってしまう…。

そんなわけで、resource.rc の最初のほうで、「_WIN32_IE」を勝手に定義して以下のように書いておけば、一応コンパイルできると分かった。「_WIN32_IE」って何なのか分かってないけど…。いや、名前からしてIE関係の何かなのだろうけど…。
/* trackbar enable */
/* trackbarを使うなら以下も記述しないと TBS_TOOLTIPS が未定義になり Syntax error が出る。 */
#define _WIN32_IE   0x0300
#include <commctrl.h>

#include <windows.h>

#include "resource.h"

// ...

ちなみに、何故かは分からんけど、windows.h を include する前に、commctrl.h を include しておかないといけない模様。順番が逆だと反映されなかった。

ソース :

そんなわけで、MinGW を使ってビルドする場合でも、trackbar を使った設定ダイアログの表示ができるようになった。サンプルソースは以下。

_resource.h
#ifndef RESOURCE_H_
#define RESOURCE_H_

#define IDC_MYICON                      2
#define IDD_DIALOGTEST01_DIALOG         102
#define IDR_MAINFRAME                   128
#define IDD_DIALOG1                     129
#define IDC_FPSDISPLAY                  2003
#define IDC_RESET                       2004
#define IDC_WAITVALUE                   2005
#define IDC_SPEED                       2006
#define IDC_NUMBER                      2007
#define IDC_STATUS1                     2008
#define IDC_STATIC                      -1

#endif

_resource.rc
/* resource */

/* trackbar enable */
/* trackbarを使うなら以下も記述しないと TBS_TOOLTIPS が未定義になり Syntax error が出る。 */
#define _WIN32_IE   0x0300
#include <commctrl.h>

#include <windows.h>

#include "resource.h"

#if 0
#define TBS_AUTOTICKS   1
#define TBS_TOOLTIPS 0x0100
#endif

/* Dialog */
IDD_DIALOG1 DIALOG DISCARDABLE  0, 0, 290, 120
STYLE DS_MODALFRAME | WS_POPUP | WS_VISIBLE | WS_CAPTION | WS_SYSMENU
CAPTION "Setting"
 FONT 11, "Segoe UI"
BEGIN
  DEFPUSHBUTTON "OK",      IDOK,      172, 96, 50, 14
  PUSHBUTTON    "Cancel",  IDCANCEL,  228, 96, 50, 14
  CTEXT "ssstars GL setting", IDC_STATIC, 7, 7, 275, 8
  LTEXT "Wait (5 - 200 msec)", IDC_STATIC, 12, 24, 72, 8
  LTEXT "Speed (100 - 4000)", IDC_STATIC, 12, 48, 72, 8
  LTEXT "Number (10 - 4000)", IDC_STATIC, 12, 72, 72, 8
  AUTOCHECKBOX  "FPS display", IDC_FPSDISPLAY, 12, 96, 53, 10
  PUSHBUTTON    "Reset",   IDC_RESET, 76,  96, 50, 14
  CONTROL "", IDC_WAITVALUE, "msctls_trackbar32", TBS_AUTOTICKS | TBS_TOOLTIPS | WS_TABSTOP, 96, 24, 180, 15
  CONTROL "", IDC_SPEED,     "msctls_trackbar32", TBS_AUTOTICKS | TBS_TOOLTIPS | WS_TABSTOP, 96, 48, 180, 15
  CONTROL "", IDC_NUMBER,    "msctls_trackbar32", TBS_AUTOTICKS | TBS_TOOLTIPS | WS_TABSTOP, 96, 72, 180, 15
END

_02_dialogtest_tb.c
#include <tchar.h>
#include <wchar.h>
#include <windows.h>
#include <stdio.h>
#include <stdlib.h>

// trackbar を使う時は commctrl.h の include が必要
// comctl32 もリンクする。(-lcomcil32)
#include <commctrl.h>

// リソースファイルを使う時は include する
#include "resource.h"

// メインウインドウ上に設置するボタンのID
#define IDC_BUTTON_OPENDLG 110

// global work グローバル変数
HINSTANCE hInst;

// static WCHAR txt[1024]; // message work
static char txt[1024]; // message work

// global paramater work
static int waitValue = 15;
static int speedValue = 1000;
static int numberValue = 1000;
static int fps_display = 1;

// ========================================
// prototype プロトタイプ宣言
LRESULT CALLBACK WndProc(HWND, UINT, WPARAM, LPARAM);
INT_PTR CALLBACK DlgProc(HWND, UINT, WPARAM, LPARAM);

// ========================================
// entry point エントリーポイント。Win32 GUIアプリは最初にここが呼ばれる
int WINAPI WinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance,
                   LPSTR lpCmdLine, int nCmdShow)
{
  TCHAR szWindowClass[] = TEXT("DIALOGTEST01"); // main window class name メインウインドウクラス名
  TCHAR szTitle[] = TEXT("DialogTest01");       // title bar text タイトルバーに表示するテキスト
  WNDCLASSEX wc;
  MSG msg;

  // set window class properties (struct)
  // ウインドウクラス構造体の設定
  {
    wc.cbSize = sizeof(wc);
    wc.style = CS_HREDRAW | CS_VREDRAW;
    wc.lpfnWndProc = WndProc; // ウインドウプロシージャを指定
    wc.cbClsExtra = 0;
    wc.cbWndExtra = 0;
    wc.hInstance = hInstance;
    wc.hCursor = LoadCursor(NULL, IDC_ARROW);
    wc.hbrBackground = (HBRUSH)(COLOR_WINDOW + 1);
    wc.lpszMenuName = NULL;
    wc.lpszClassName = szWindowClass;
    wc.hIcon = NULL; // 今回はアイコンを指定しない
    wc.hIconSm = NULL;
    // wc.hIcon = LoadIcon(hInstance, MAKEINTRESOURCE(IDI_APP_ICON));
    // wc.hIconSm = LoadIcon(wc.hInstance, MAKEINTRESOURCE(IDI_SMALL));

    if (RegisterClassEx(&wc) == 0)
      return FALSE;
  }

  // create window ウインドウを作成
  {
    hInst = hInstance;

    HWND hWnd = CreateWindow(
        szWindowClass,
        szTitle, // タイトルバーのテキストを指定
        WS_OVERLAPPEDWINDOW,
        CW_USEDEFAULT, CW_USEDEFAULT, // x, y
        512, 288,                     // width, height
        NULL, NULL, hInstance, NULL);

    if (hWnd == NULL)
      return FALSE;

    // display window ウインドウを表示
    ShowWindow(hWnd, nCmdShow);
    UpdateWindow(hWnd);
  }

  // main message loop メッセージループ
  {
    BOOL bRet;
    while ((bRet = GetMessage(&msg, NULL, 0, 0)) != 0)
    {
      if (bRet == -1)
      {
        break;
      }
      else
      {
        TranslateMessage(&msg);
        DispatchMessage(&msg);
      }
    }
  }

  return (int)msg.wParam;
}

// ========================================
// main window procedure
// メインウインドウプロシージャ。メインウインドウ上で何かが起きるとここが呼ばれる
LRESULT CALLBACK WndProc(HWND hWnd, UINT message, WPARAM wParam, LPARAM lParam)
{
  // static WCHAR txt[1024];

  switch (message)
  {
  case WM_CREATE:
    // create main window. initialize
    // メインウインドウ作成時にここに来る。初期化処理をする

    // create "Open Dialog" button ボタンを1つ設置する
    CreateWindow(
        "BUTTON", "Open Dialog", // クラス名、ボタン上に表示するテキスト
        WS_CHILD | WS_VISIBLE,   // スタイル
        20, 20, 100, 24,         // x, y, width, height
        hWnd,
        (HMENU)IDC_BUTTON_OPENDLG, // ボタンに紐づけるID
        hInst,
        NULL);
    break;

  case WM_COMMAND:
  {
    // application menu process

    int wmId = LOWORD(wParam);
    switch (wmId)
    {
    case IDC_BUTTON_OPENDLG:
    {
      // push "Open Dialog" button
      // "Open Dialog" ボタンが押されるとここに来る

      // Open dialog リソースファイル内で記述されてるダイアログを開く
      int ret;
      ret = DialogBox(hInst, MAKEINTRESOURCE(IDD_DIALOG1), hWnd, DlgProc);

      // モーダルダイアログを開いているので、OK か Cancel が押されるまで処理は返ってこない

      // dialog results ダイアログが返してきた値で処理を分ける
      switch (ret)
      {
      case IDOK:
        // push "OK" button OKボタンが押された
        strcpy(txt, "IDOK");
        // wcscpy_s(txt, TEXT("IDOK"));
        break;

      case IDCANCEL:
        // push "Cancel" button Cancelボタンが押された
        strcpy(txt, "IDCANCEL");
        // wcscpy_s(txt, TEXT("IDCANCEL"));
        break;
      }
      InvalidateRect(hWnd, NULL, TRUE);
    }
    break;

    default:
      return DefWindowProc(hWnd, message, wParam, lParam);
    }
  }
  break;

  case WM_PAINT:
  {
    // draw main window メインウインドウ描画処理

    PAINTSTRUCT ps;
    HDC hdc = BeginPaint(hWnd, &ps);

    // draw message "IDOK" or "IDCANCEL" のメッセージを表示
    // TextOut(hdc, 20, 70, txt, lstrlen(txt));
    TextOut(hdc, 20, 70, txt, strlen(txt));

    // draw paramater value ダイアログ上で設定した値を表示
    {
      char s[256];

      sprintf(s, "wait=%d", waitValue);
      TextOut(hdc, 20, 90, s, strlen(s));

      sprintf(s, "speed=%d", speedValue);
      TextOut(hdc, 20, 110, s, strlen(s));

      sprintf(s, "number=%d", numberValue);
      TextOut(hdc, 20, 130, s, strlen(s));

      sprintf(s, "fps_display=%d", fps_display);
      TextOut(hdc, 20, 150, s, strlen(s));
    }

    EndPaint(hWnd, &ps);
  }
  break;

  case WM_DESTROY:
    // destroy window ウインドウ破棄時の処理
    PostQuitMessage(0);
    break;

  default:
    return DefWindowProc(hWnd, message, wParam, lParam);
  }
  return 0;
}

// set value to config dialog
// 設定ダイアログ上の各値を設定する
void SetvalueToDialog(HWND hDlg, int waitValue, int speedValue, int numberValue, int fps_display)
{
  // set trackbar ("msctls_trackbar32")
  // trackbarの範囲や値を設定
  {
    HWND cHwnd;

    cHwnd = GetDlgItem(hDlg, IDC_WAITVALUE);                    // get hundle
    SendMessage(cHwnd, TBM_SETRANGE, TRUE, MAKELPARAM(5, 200)); // set range
    SendMessage(cHwnd, TBM_SETTICFREQ, 5, 0);                   // set tick
    SendMessage(cHwnd, TBM_SETPOS, TRUE, (LPARAM)waitValue);    // set position

    cHwnd = GetDlgItem(hDlg, IDC_SPEED);
    SendMessage(cHwnd, TBM_SETRANGE, TRUE, MAKELPARAM(100, 4000));
    SendMessage(cHwnd, TBM_SETTICFREQ, 100, 0);
    SendMessage(cHwnd, TBM_SETPOS, TRUE, (LPARAM)speedValue);

    cHwnd = GetDlgItem(hDlg, IDC_NUMBER);
    SendMessage(cHwnd, TBM_SETRANGE, TRUE, MAKELPARAM(10, 4000));
    SendMessage(cHwnd, TBM_SETTICFREQ, 100, 0);
    SendMessage(cHwnd, TBM_SETPOS, TRUE, (LPARAM)numberValue);
  }

  // set checkbox
  // CHECKBOXの状態を初期化する
  // GetDlgItem() でコントロールのハンドルを取得できる
  // SendMessage() でコントロールの状態を変更できる
  // BM_SETCHECK で状態の変更を指示
  {
    HWND cHwnd;
    cHwnd = GetDlgItem(hDlg, IDC_FPSDISPLAY);
    SendMessage(cHwnd, BM_SETCHECK, ((fps_display == 0) ? BST_UNCHECKED : BST_CHECKED), 0);
  }
}

// get value from confgi dialog
// 設定ダイアログ上の各値を取得する
void GetValueFromDialog(HWND hDlg)
{
  // get trackbar value
  // trackbar の値を取得してグローバル変数に記録
  // GetDlgItemText() でEDITTEXTの内容(文字列)を取得
  waitValue = SendMessage(GetDlgItem(hDlg, IDC_WAITVALUE), TBM_GETPOS, 0, 0);
  speedValue = SendMessage(GetDlgItem(hDlg, IDC_SPEED), TBM_GETPOS, 0, 0);
  numberValue = SendMessage(GetDlgItem(hDlg, IDC_NUMBER), TBM_GETPOS, 0, 0);

  // get checkbox status
  // CHECKBOXの状態をグローバル変数に記録
  // IsDlgButtonChecked() で CHECKBOX の状態を取得できる
  fps_display = (IsDlgButtonChecked(hDlg, IDC_FPSDISPLAY) == BST_CHECKED) ? 1 : 0;
}

// ========================================
// dialog procedure
// ダイアログプロシージャ。ダイアログ上でボタン等が押された際に呼ばれる
INT_PTR CALLBACK DlgProc(HWND hDlg, UINT message, WPARAM wParam, LPARAM lParam)
{
  switch (message)
  {
  case WM_INITDIALOG:
  {
    // initialize dialog
    // ダイアログ初期化時の処理。GUI部品の状態を設定

    InitCommonControls(); // trackbarを使う時はどこかで一度呼んでおくらしい

    // 設定ダイアログ上の値を設定
    SetvalueToDialog(hDlg, waitValue, speedValue, numberValue, fps_display);
  }
    return TRUE;

  case WM_COMMAND:
    switch (LOWORD(wParam))
    {
    case IDOK:
      // push "OK" button "OK"ボタンが押されたらここに来る
      GetValueFromDialog(hDlg); // 設定ダイアログ上の値を取得してグローバル変数に記録
      EndDialog(hDlg, IDOK);    // ダイアログを終了
      return TRUE;

    case IDCANCEL:
      // push "Cancel" button
      EndDialog(hDlg, IDCANCEL); // ダイアログを終了
      return TRUE;

    case IDC_RESET:
    {
      // push "Reset" button "Reset"ボタンが押された

      // change status text
      // ダイアログ上のLABELのテキストを変更
      SetWindowText(GetDlgItem(hDlg, IDC_STATUS1), TEXT("Reset"));

      // ダイアログ上の値をデフォルト値に変更
      SetvalueToDialog(hDlg, 15, 1000, 1000, 1);
    }
      return TRUE;
    }
    break;
  }
  return FALSE;
}

_Makefile
PROGRAM=02_dialogtest_tb.exe

SRC=$(PROGRAM:.exe=.c)
OBJS=$(SRC:.c=.o) resource.o

.PHONY: all
all: $(PROGRAM)

$(PROGRAM): $(OBJS) Makefile
    gcc $(OBJS) -lcomctl32 -mwindows -o $@

%.o: %.c
    gcc -O3 -o $@ -c $<

resource.o: resource.rc Makefile
    windres $< -o $@

.PHONY: cleanall clean

cleanall: clean
    rm -f $(PROGRAM)

.PHONY: clean
clean:
    rm -f $(OBJS)

make と打てば 02_dialogtest_tb.exe をビルドできる。実行結果は以下。

#2 [prog] スライダー・トラックバーは単体のみで使い物になるのだろうか

スライダー/トラックバー(以下、スライダーと記述)をダイアログ上に表示する方法について、ここ数日色々調べていたけれど。はたしてスライダーだけあれば本当に便利になるのか? それは快適に使えるのか? 目的は果たせるのか? そんな疑問が湧いてしまった。

スライダーのメリットは何だろう? ざっくりとした数値指定ができることかなと…。バーをマウス操作で一気に動かせば、数値を大きく変化させることができる。また、指定可能な値の範囲のうち、今現在どのあたりの数値を指定しているのか、視覚的に分かりやすい。

ただ、数値を細かく変化させたい時は苦痛だろう…。マウスを小さく動かしたり、カーソルキーを何度か叩くことになる。そもそもスライダーの長さによっては、値を+1/-1することができないこともある。100ドットの幅しかないスライダーで、0 - 10000 の範囲を指定したら、バーを1ドット動かすだけで、数値が100ぐらい変わってしまう。

厳密な数値指定はできなくて、あくまでざっくりとした、大雑把な数値指定しかできない。そこがデメリットだろう…。

であれば、別途数値入力欄も用意しておいて、以下のような処理も書かないと、使い勝手が悪いのではないか…。
でも、そのあたりの処理を書くのは、なんだか面倒臭い。

もしかすると、無理矢理スライダーを使わずに、単に数値入力欄だけを用意しておいて、直接数値を打ち込んでもらったほうが間違いなく操作できるのではないか、という気もする…。

既存事例 :

既存のアプリでは、スライダーの扱いはどうなっているのか気になった。いくつか調べてみた。

ドットエディタ EDGE2 のカラーパレット周り。

trackbar_ss_edge2.png
  • スライダー相当の左に数値入力欄がある。
  • スライダーを動かすと、数値入力欄の表示も変わる。
  • 数値入力欄に値を打ち込むと、スライダーの状態も変化する。
  • スライダーの左右に小さなボタンがあって、このボタンをクリックすると-1/+1で数値を変化させられる。


G'MICという画像フィルタ集のUI。

trackbar_ss_gmic.png
  • スライダーの右側にスピンボックス(SpinBox、数値入力欄の右端に小さな2つのボタンがあって、値を+1/-1できる)がついている。
  • スライダーを動かすと、スピンボックスの表示も変わる。
  • スピンボックスに数値を入力すると、スライダーの状態も変わる。
  • スピンボックスの2つのボタンをクリックすると、数値を+1/-1できる。


GIMP 2.10.34 のUI。

trackbar_ss_gimp.png

スライダーに重ねる形で数値入力欄、+1/-1ができる小さな2つのボタンがついている。余談だけど、GIMP のこのUIは、数値部分を右クリックすると数値入力モードになる。


AzPainter2 のUI。

trackbar_ss_azpainter2.png

GIMPと似た感じで、スライダーに重ねる形で数値表示、+1/-1ができる小さな2つのボタンがついている。あるいは、スライダーの右にスピンボックスが並べてある。


恥ずかしながら今までこのあたり全然意識してなかったけど、どのアプリを眺めてみても、スライダーの横には数値入力欄、もしくは数値表示欄があって、かつ、値を+1/-1できるボタンがついてる場合が多い、と分かった。

つまり、スライダーだけあれば事足りる場面は、実はそんなにないのだろう…。ほとんどの場合、スライダーと一緒に以下のウィジェット(GUI部品、コントロール)も必要になってしまうのではないか。
  • スライダー + 数値入力欄 + +1/-1用の2つのボタン
  • または、スライダー + スピンボックス

最初から全部一緒になったウィジェットを用意しておいてくれれば楽なのではないかと思えてきた…。

2024/01/18(木) [n年前の日記]

#1 [prog] トラックバーの横に数値入力欄をつけて連動できるか試した

_昨日、 トラックバー/スライダーだけでは値の指定が難しい場面もあるのではないか、数値入力欄やスピンボックスも必要になるのではないかと考えてしまったのだけど。実装はどのくらい大変なのか、ちょっと気になってしまったので、そのあたりを試してみた。

環境は、Windows10 x64 22H2 + Visual Studio Community 2019。

ダイアログ上では、トラックバー("msctls_trackbar32")、数値入力欄(EDITTEXT)、スピンコントロール(UpDownコントロール?、"msctls_updown32")を横に並べて配置した。これらを連動させないといけない…。

実行結果 :

先に実行結果を見せておく。以下のような動作になった。どれも連動しているように見える。

ソース :

とりあえず動いてるように見えるので、一応ソースを置いておく。このやり方で正しいのかどうかは分からんけど…。

_resource.h
//{{NO_DEPENDENCIES}}
// Microsoft Visual C++ で生成されたインクルード ファイル。
// trackbarandspinbox.rc で使用
//
#define IDC_MYICON                      2
#define IDD_TRACKBARANDSPINBOX_DIALOG   102
#define IDS_APP_TITLE                   103
#define IDC_TRACKBARANDSPINBOX          109
#define IDR_MAINFRAME                   128
#define IDD_DIALOG1                     129
#define IDC_SLIDER1                     1000
#define IDC_EDIT1                       1001
#define IDC_SPIN1                       1002
#define IDC_STATIC                      -1

// Next default values for new objects
// 
#ifdef APSTUDIO_INVOKED
#ifndef APSTUDIO_READONLY_SYMBOLS
#define _APS_NO_MFC                     1
#define _APS_NEXT_RESOURCE_VALUE        130
#define _APS_NEXT_COMMAND_VALUE         32771
#define _APS_NEXT_CONTROL_VALUE         1003
#define _APS_NEXT_SYMED_VALUE           110
#endif
#endif


以下のリソースファイル内の IDD_DIALOG1 のあたりが、ダイアログの指定。

_trackbarandspinbox.rc
// Microsoft Visual C++ generated resource script.
//
#include "resource.h"

#define APSTUDIO_READONLY_SYMBOLS
/////////////////////////////////////////////////////////////////////////////
//
// Generated from the TEXTINCLUDE 2 resource.
//
#ifndef APSTUDIO_INVOKED
#include "targetver.h"
#endif
#define APSTUDIO_HIDDEN_SYMBOLS
#include "windows.h"
#undef APSTUDIO_HIDDEN_SYMBOLS

/////////////////////////////////////////////////////////////////////////////
#undef APSTUDIO_READONLY_SYMBOLS

/////////////////////////////////////////////////////////////////////////////
// 日本語 (日本) resources

#if !defined(AFX_RESOURCE_DLL) || defined(AFX_TARG_JPN)
LANGUAGE LANG_JAPANESE, SUBLANG_DEFAULT

/////////////////////////////////////////////////////////////////////////////
//
// Dialog
//

IDD_DIALOG1 DIALOGEX 0, 0, 325, 66
STYLE DS_SETFONT | DS_MODALFRAME | WS_POPUP | WS_CAPTION | WS_SYSMENU
CAPTION "Dialog"
 FONT 11, "Segoe UI", 400, 0, 0x0
BEGIN
    DEFPUSHBUTTON   "OK",IDOK,204,42,50,14
    PUSHBUTTON      "Cancel",IDCANCEL,261,42,50,14
    CONTROL         "",IDC_SLIDER1,"msctls_trackbar32",TBS_AUTOTICKS | TBS_BOTH | TBS_TOOLTIPS | WS_TABSTOP,36,18,188,12
    EDITTEXT        IDC_EDIT1,225,18,54,12,ES_AUTOHSCROLL | ES_NUMBER
    CONTROL         "",IDC_SPIN1,"msctls_updown32",UDS_SETBUDDYINT | UDS_ALIGNRIGHT | UDS_ARROWKEYS | UDS_NOTHOUSANDS,280,18,9,12
END


/////////////////////////////////////////////////////////////////////////////
//
// DESIGNINFO
//

#ifdef APSTUDIO_INVOKED
GUIDELINES DESIGNINFO
BEGIN
    IDD_DIALOG1, DIALOG
    BEGIN
        LEFTMARGIN, 7
        RIGHTMARGIN, 318
        TOPMARGIN, 7
        BOTTOMMARGIN, 59
    END
END
#endif    // APSTUDIO_INVOKED


#ifdef APSTUDIO_INVOKED
/////////////////////////////////////////////////////////////////////////////
//
// TEXTINCLUDE
//

1 TEXTINCLUDE 
BEGIN
    "resource.h\0"
END

2 TEXTINCLUDE 
BEGIN
    "#ifndef APSTUDIO_INVOKED\r\n"
    "#include ""targetver.h""\r\n"
    "#endif\r\n"
    "#define APSTUDIO_HIDDEN_SYMBOLS\r\n"
    "#include ""windows.h""\r\n"
    "#undef APSTUDIO_HIDDEN_SYMBOLS\r\n"
    "\0"
END

3 TEXTINCLUDE 
BEGIN
    "\r\n"
    "\0"
END

#endif    // APSTUDIO_INVOKED


/////////////////////////////////////////////////////////////////////////////
//
// String Table
//

STRINGTABLE
BEGIN
    IDS_APP_TITLE           "trackbarandspinbox"
    IDC_TRACKBARANDSPINBOX  "TRACKBARANDSPINBOX"
END

#endif    // 日本語 (日本) resources
/////////////////////////////////////////////////////////////////////////////



#ifndef APSTUDIO_INVOKED
/////////////////////////////////////////////////////////////////////////////
//
// Generated from the TEXTINCLUDE 3 resource.
//


/////////////////////////////////////////////////////////////////////////////
#endif    // not APSTUDIO_INVOKED


_trackbarandspinbox.cpp
// trackbarandspinbox.cpp
//
// trackbar + spinbox(edit + spin control) sample

#include <Windows.h>
#include <CommCtrl.h>

#include "framework.h"
#include "trackbarandspinbox.h"

#define MAX_LOADSTRING 100

#define IDC_BUTTON_OPENDIALOG   110

// グローバル変数:
HINSTANCE hInst;                                // 現在のインターフェイス
WCHAR szTitle[MAX_LOADSTRING];                  // タイトル バーのテキスト
WCHAR szWindowClass[MAX_LOADSTRING];            // メイン ウィンドウ クラス名

static int number = 1000;

// このコード モジュールに含まれる関数の宣言を転送します:
LRESULT CALLBACK    WndProc(HWND, UINT, WPARAM, LPARAM);
INT_PTR CALLBACK    DlgProc(HWND, UINT, WPARAM, LPARAM);

// entry point
int APIENTRY wWinMain(_In_ HINSTANCE hInstance,
    _In_opt_ HINSTANCE hPrevInstance,
    _In_ LPWSTR    lpCmdLine,
    _In_ int       nCmdShow)
{
    UNREFERENCED_PARAMETER(hPrevInstance);
    UNREFERENCED_PARAMETER(lpCmdLine);

    // TODO: ここにコードを挿入してください。

    // グローバル文字列を初期化する
    LoadStringW(hInstance, IDS_APP_TITLE, szTitle, MAX_LOADSTRING);
    LoadStringW(hInstance, IDC_TRACKBARANDSPINBOX, szWindowClass, MAX_LOADSTRING);

    // ウインドウクラスを登録
    {
        WNDCLASSEXW wcex;

        wcex.cbSize = sizeof(WNDCLASSEX);
        wcex.style = CS_HREDRAW | CS_VREDRAW;
        wcex.lpfnWndProc = WndProc;
        wcex.cbClsExtra = 0;
        wcex.cbWndExtra = 0;
        wcex.hInstance = hInstance;
        wcex.hIcon = NULL;                              // icon無し
        wcex.hCursor = LoadCursor(nullptr, IDC_ARROW);
        wcex.hbrBackground = (HBRUSH)(COLOR_WINDOW + 1);
        wcex.lpszMenuName = NULL;                       // menu無し
        wcex.lpszClassName = szWindowClass;
        wcex.hIconSm = NULL;                            // icon無し

        RegisterClassExW(&wcex);
    }

    // メインウインドウ生成
    {
        hInst = hInstance;

        HWND hWnd = CreateWindowW(
            szWindowClass,
            szTitle,
            WS_OVERLAPPEDWINDOW,
            CW_USEDEFAULT, CW_USEDEFAULT, // x, y
            320, 180, // width, height
            nullptr,
            nullptr,
            hInstance,
            nullptr);

        if (!hWnd)
        {
            return FALSE;
        }

        ShowWindow(hWnd, nCmdShow);
        UpdateWindow(hWnd);
    }

    MSG msg;

    // メイン メッセージ ループ:
    while (GetMessage(&msg, nullptr, 0, 0))
    {
        TranslateMessage(&msg);
        DispatchMessage(&msg);
    }

    return (int)msg.wParam;
}


//
//  関数: WndProc(HWND, UINT, WPARAM, LPARAM)
//
//  目的: メイン ウィンドウのメッセージを処理します。
//
//  WM_COMMAND  - アプリケーション メニューの処理
//  WM_PAINT    - メイン ウィンドウを描画する
//  WM_DESTROY  - 中止メッセージを表示して戻る
//
//
LRESULT CALLBACK WndProc(HWND hWnd, UINT message, WPARAM wParam, LPARAM lParam)
{
    switch (message)
    {
    case WM_CREATE:
        // メインウインドウ生成時の処理

        // ”Open Dialog" ボタンを配置
        CreateWindow(
            L"BUTTON", L"Open Dialog",
            WS_CHILD | WS_VISIBLE,
            20, 20, 100, 24, // x, y, width, height
            hWnd,
            (HMENU)IDC_BUTTON_OPENDIALOG, // ボタンに割り当てるID
            hInst,
            NULL
        );
        break;

    case WM_COMMAND:
    {
        int wmId = LOWORD(wParam);
        switch (wmId)
        {
        case IDC_BUTTON_OPENDIALOG:
            // "Open Dialog"ボタンが押されたのでダイアログを表示
            DialogBox(hInst, MAKEINTRESOURCE(IDD_DIALOG1), hWnd, DlgProc);

            InvalidateRect(hWnd, NULL, TRUE); // これを入れないとメインウインドウが再描画されない
            break;
        default:
            return DefWindowProc(hWnd, message, wParam, lParam);
        }
    }
    break;
    case WM_PAINT:
        // メインウインドウ描画処理
    {
        PAINTSTRUCT ps;
        HDC hdc = BeginPaint(hWnd, &ps);

        // TODO: HDC を使用する描画コードをここに追加してください...

        // 設定値を描画
        {
            wchar_t szBuf[1024];
            swprintf_s(szBuf, sizeof(szBuf) / sizeof(wchar_t), L"number=%d", number);
            TextOut(hdc, 20, 70, szBuf, lstrlen(szBuf));
        }

        EndPaint(hWnd, &ps);
    }
    break;

    case WM_DESTROY:
        PostQuitMessage(0);
        break;

    default:
        return DefWindowProc(hWnd, message, wParam, lParam);
    }
    return 0;
}

INT_PTR CALLBACK DlgProc(HWND hDlg, UINT message, WPARAM wParam, LPARAM lParam)
{
    static HWND hEdit1, hSlider1, hSpin1;
    int n;
    BOOL success;

    UNREFERENCED_PARAMETER(lParam);
    switch (message)
    {
    case WM_INITDIALOG:
        // ダイアログ生成時の処理
        hSlider1 = GetDlgItem(hDlg, IDC_SLIDER1);
        hEdit1 = GetDlgItem(hDlg, IDC_EDIT1);
        hSpin1 = GetDlgItem(hDlg, IDC_SPIN1);

        // SpinBox(UpDOwnコントロール)を設定
        SendMessage(hSpin1, UDM_SETBUDDY, (WPARAM)hEdit1, 0);  // EDITTEXTと連動
        SendMessage(hSpin1, UDM_SETRANGE, (WPARAM)0, (LPARAM)MAKELONG(4000, 10)); // 値の範囲を設定
        SendMessage(hSpin1, UDM_SETPOS, 0, (LPARAM)number); // 現在の値を設定

        // trackbarを設定
        SendMessage(hSlider1, TBM_SETRANGE, TRUE, (LPARAM)MAKELONG(10, 4000)); // 値の範囲を設定
        SendMessage(hSlider1, TBM_SETTICFREQ, 500, 0);  // 目盛りの間隔を設定
        SendMessage(hSlider1, TBM_SETPOS, TRUE, (LPARAM)number); // 現在の値を設定

        return (INT_PTR)TRUE;

    case WM_HSCROLL:
        // trackbarが操作された
        if ((HWND)(lParam) == hSlider1) {
            int n;
            n = SendMessage(hSlider1, TBM_GETPOS, 0, 0);  // get trackbar pos
            SendMessage(hSpin1, UDM_SETPOS, 0, (LPARAM)n);  // set SpiBox pos
        }
        return (INT_PTR)TRUE;

    case WM_COMMAND:
        // ダイアログからコマンドを受け取る
        switch (LOWORD(wParam))
        {
        case IDOK:
            // OKボタンが押された
            n = GetDlgItemInt(hDlg, IDC_EDIT1, &success, FALSE);
            if (success)
                number = n;

            EndDialog(hDlg, LOWORD(wParam)); // ダイアログを終了
            return (INT_PTR)TRUE;

        case IDCANCEL:
            // Cancelボタンが押された
            EndDialog(hDlg, LOWORD(wParam)); // ダイアログを終了
            return (INT_PTR)TRUE;

        case IDC_EDIT1:
            // EDIT欄が変更された
            n = GetDlgItemInt(hDlg, IDC_EDIT1, &success, FALSE); // get edit int value
            if (success)
                SendMessage(hSlider1, TBM_SETPOS, TRUE, (LPARAM)n); // set trackbar pos
            return (INT_PTR)TRUE;

        default:
            break;
        }
        break;
    }
    return (INT_PTR)FALSE;
}


_framework.h
_targetver.h
_trackbarandspinbox.h

少し解説 :

ダイアログプロシージャ、DlgProc() の中を見れば、やってることが分かるかなと…。

スピンコントロール(UpDownコントロール)は、どの入力欄(EDITTEXT)と連動させるかを指定できる。SendMessage() に UDM_SETBUDDY を指定することで、連動させるコントロールを設定する。ただ、その場合、リソースファイル(.rc)内のスピンコントロールについては、以下のスタイルを指定しておかないといけない。UDS_SETBUDDYINT というのが、他のコントロールと連動させるための指定だろうか。
UDS_SETBUDDYINT | UDS_ALIGNRIGHT | UDS_ARROWKEYS | UDS_NOTHOUSANDS

スピンコントロールは、値が変化する範囲を指定できる。このあたり、トラックバーとよく似ている。

更に、スピンコントロールは、現在の値を保持している。これもまた、トラックバーと似ている。

つまり、トラックバーとスピンコントロールは、見た目は違うけれど、内部的に持っているものがほぼ同じ、似たもの同士と言えそう。

トラックバー(横向き)が操作されると、WM_HSCROLL というメッセージが送られてくる。ちなみに縦向きのトラックバーなら、WM_VSCROLL が送られてくるらしい。

その際、lParam には、そのメッセージを送ってきたトラックバーのハンドルが入っているので、そのハンドルを利用して対象となるトラックバーを絞り込んで(?)、トラックバーの現在値を、SendMessage() を使って取得することができる。

トラックバーの現在値が得られたら、それをスピンコントロールの現在値として設定してやれば、入力欄(EDITTEXT)の表示も自動的にその値になってくれる。これで、トラックバーの操作を入力欄に反映させることはできた。本当にこのやり方でいいのか分からんけど、一応それっぽく動いてる。

スピンコントロールの2つのボタンをクリックすると、入力欄(EDITTEXT)上の値も変化する。あるいは、入力欄にカーソルを合わせて、直接数値を打ち込んでもいいけど…。何にせよ、入力欄の値が変化すると、WM_COMMAND と、その入力欄のIDが送られてくる。入力欄の数値を int として読み取れたら、その値をトラックバーの現在値に設定してやる。これで、入力欄の操作や、スピンコントロールの操作を、トラックバーにも反映させることができた。本当にこのやり方でいいのか分からんけど。

そんな感じで…。
  • スピンコントロールの操作を、入力欄に反映。
  • 入力欄の操作を、トラックバーに反映。
  • トラックバーの操作を、スピンコントロールに反映。
三種類のコントロールを連動させることができた。

しかし…。一々こんな風に書かないといけないものだろうか?

巷のアプリは、もっと膨大な数のトラックバーやスピンボックスがずらりと並んでいると思うのだけど。その一つ一つにこんな処理を毎回書いていくのは、さすがに大変なのでは…。何かもっと上手いやり方、便利な方法が用意されてそうな気がする…。

余談。入力欄の種類 :

巷の解説ページを見ていて気付いたけれど、入力欄(EDITTEXT)に、ES_NUMBER というスタイル指定を追加してやれば、その入力欄は数字しか受け付けない/アルファベットを打っても拒否してくれる入力欄になってくれるっぽい。数値しか扱わない EDITTEXT にしたい場合は指定したほうが良さそう。

参考ページ :

#2 [prog] iniファイルが妙な場所に作られてしまって悩んだ

C/C++を使った場合のiniファイルの読み書きの仕方も分かったし、設定ダイアログ上の値を読み書きする方法も分かったので、先日作成した「宇宙飛行」スクリーンセーバモドキのOpenGL描画プログラムをスクリーンセーバ化しようと作業を始めたのだけど…。

C:\Window\SysWOW64\ 以下に、スクリーンセーバのプログラム、.scr と、設定ファイル、.ini をコピーして、スクリーンセーバから .ini に設定を書き込もうとしたものの、.ini の内容が全く更新されなくて悩んでしまった。

スクリーンセーバの設定値は、たしかに変更されていて、設定ダイアログを開いても設定値がちゃんと反映されているのに、.ini の内容は以前と変わってない。何故。

試しに .ini ファイルを削除してみたけれど、その後もまるで .ini があるかのように動作してしまう。設定した値も読み取れている。しかし .ini ファイルは存在していない…。どういうこと…?

おそらくはどこか謎の場所に .ini があって、それを読み書きしているのだろうけど、一体どの場所に存在しているのか…。

Cドライブ内を検索しまくったところ、何故か以下のフォルダに .ini があった…。
C:\Users\(USERNAME)\AppData\Local\VirtualStore\Windows\SysWOW64\ssstars_opengl.ini

VirtualStoreのせいだった :

ググってみたところ、Windows Vista以降は、各種権限が厳しくなってシステム絡みのフォルダに書き込みができなくなっていたそうで。しかしそれでは昔のアプリが動かなくなってしまうので、VirtualStore という機能?を追加して、そちらを読み書きすることでどうにかしていたらしい…。

_Windows の VirtualStore 機能 - ラボラジアン
_VirtualStoreにやられた。。。 - ak days

C:\Windows\SysWOW64\ 内の .ini を読み書きしているつもりが、実際には、ユーザーフォルダ\AppData\Local\VirtualStore\ 内の .ini を読み書きしていたのだな…。だから C:\Windows\SysWOW64\ 以下の .ini を削除しても、何事もなかったように動いたわけで…。

さて、どうしたもんか。どうせユーザフォルダ内の VirtualStore\*.ini ファイルにアクセスしているなら、最初からユーザフォルダ内に設定ファイルを保存して読み書きするようにしてしまったほうがいいよな…。

となると、ユーザフォルダのPATHを取得する方法、AppData\Roaming\ 以下にスクリーンセーバ名でディレクトリを作る方法について調べないと…。

いや、本当にそれでいいのだろうか。もしユーザ名が日本語だった場合、正常にPATHを取得して読み書きできるのだろうか。それに、ユーザフォルダ内に謎の設定ファイルが勝手に作られるスクリーンセーバもなんだか怖くないか。アンインストールしたい場合はどうすればいいのか。

そう考えると、C:\Windows\SysWOW64\ 内に設定ファイルもあるものとして作っておいて、Windows Vista以降はここに設定ファイルがありますよとドキュメントで一言触れておくだけのほうがマシだったりしないか…。このあたり、どの方針が妥当なのだろう。

#3 [prog] スクリーンセーバのフレームレートが安定しない

C/C++ と OpenGL でWindows用スクリーンセーバを作っているところだけど、何故かフレームレートが安定しない。

スクリーンセーバ設定ウインドウを表示して、「プレビュー」をクリックした時は、60FPSで動いてくれるのだけど。実際に時間が経ってスクリーンセーバとして呼び出されると、40 - 50FPS で描画されてしまう。おそらくはバッググラウンドで何かがせっせと動いていて負荷がかかっているか、もしくはアイドル状態になるとCPUその他が省電力に切り替わってパフォーマンスが出ないのではないかと想像しているけれど…。

何にせよ、処理落ちっぽい見た目になって動きがギクシャクしてしまうので、前回のフレームと今回のフレームの時間差をミリ秒で取得してオブジェクトの速度に反映させてしまうことにした。時間取得は timeGetTime() を利用。1000 で割って、0.xxxx の値にして、1秒間あたりの移動量に掛けてやる。この値をそのフレームの移動速度にする。

_【Windows】実行時間の計測 #Windows - Qiita
_C/C++ programming technique @ catfish
_C言語/C++ 処理時間計測 入門

これで多少はマシになった。まだちょっとギクシャク感はあるけれど、明らかな処理落ち状態よりマシだろう…。

2024/01/19(金) [n年前の日記]

#1 [prog] C/C++で特殊フォルダ内にiniファイルを作成して読み書きしてみた

C/C++で、Windowsの特殊フォルダ、%APPDATA% のPATHを取得して、その中に iniファイルを作成して読み書きできるのか試してみた。

環境は、Windows10 x64 22H2 + MinGW (gcc 6.3.0)。

%APPDATA% は、Windows10の場合、C:\Users\(USERNAME)\AppData\Roaming になることが多いらしい。Windows XP の場合は、また違う場所になるらしいけど…。

_上野家のホームページ - 資料室 : PC/Windows/Vista/VistaとXPのフォルダ相違VistaとXPにおけるフォルダの相違
_AppData 直下の Local, LocalLow, Roaming フォルダの違い
_AppDataフォルダ直下のLocal/LocalLow/Roamingサブフォルダの違いについて。 - 量産メモ帳

ソース :

ソースは以下のようになった。

_03_write_ini.cpp
// ini file read and write sample. support wchar_t.
// write C:\Users\(USERNAME)\AppData\Roaming\dev_ini_file_rw\dev_ini_file_rw.ini

// use SHGetSpecialFolderPath()
#define _WIN32_IE 0x0400

#include <shlobj.h>
#include <shlwapi.h>
#include <windows.h>
#include <stdio.h>
#include <stdlib.h>
#include <wchar.h>
#include <tchar.h>

// target directory
#define INIDIR _T("dev_ini_file_rw")

// target ini file name
#define INIFILENAME _T("dev_ini_file_rw.ini")

// section name
#define SECNAME _T("ssstarsgl_config")

// global work
int wait = 15;
int speed = 1000;
int number = 500;
int fps_display = 0;

// write or create ini file
void writeIni(TCHAR *filepath)
{
  TCHAR buf[1024];

  wsprintf(buf, _T("%d"), wait);
  WritePrivateProfileString(SECNAME, _T("wait"), buf, filepath);

  wsprintf(buf, _T("%d"), speed);
  WritePrivateProfileString(SECNAME, _T("speed"), buf, filepath);

  wsprintf(buf, _T("%d"), number);
  WritePrivateProfileString(SECNAME, _T("number"), buf, filepath);

  wsprintf(buf, _T("%d"), fps_display);
  WritePrivateProfileString(SECNAME, _T("fps_display"), buf, filepath);
}

// read ini file
void readIni(TCHAR *filepath)
{
  wait = GetPrivateProfileInt(SECNAME, _T("wait"), -1, filepath);
  speed = GetPrivateProfileInt(SECNAME, _T("speed"), -1, filepath);
  number = GetPrivateProfileInt(SECNAME, _T("number"), -1, filepath);
  fps_display = GetPrivateProfileInt(SECNAME, _T("fps_display"), -1, filepath);
}

int main(void)
{
  TCHAR filepath[MAX_PATH];        // target ini file path
  TCHAR waFolderPath[MAX_PATH];    // APPDATA folder path
  TCHAR waTgtFolderPath[MAX_PATH]; // target folder path

  // get APPDATA folder path
  // request shlobj.h
  SHGetSpecialFolderPath(NULL, waFolderPath, CSIDL_APPDATA, FALSE);
  // wprintf(L"APPDATA folder path: [%S]\n", waFolderPath);

  if (PathCombine(waTgtFolderPath, waFolderPath, INIDIR) == NULL)
  {
    printf("ERROR: Can not make target folder path.\n");
    return -1;
  }
  // wprintf(L"Target folder path: [%S]\n", waTgtFolderPath);

  {
    BOOL create_dir = FALSE;

    if (!PathFileExists(waTgtFolderPath))
    {
      printf("Not found target folder. create.\n");
      create_dir = TRUE;
    }
    else if (!PathIsDirectory(waTgtFolderPath))
    {
      printf("Found target folder path. but, not directory. create.\n");
      create_dir = TRUE;
    }

    if (create_dir)
    {
      if (!CreateDirectory(waTgtFolderPath, NULL))
      {
        printf("ERROR: Can not create target folder.\n");
        return -1;
      }
    }
  }

  if (PathCombine(filepath, waTgtFolderPath, INIFILENAME) == NULL)
  {
    printf("ERROR: Can not make ini file path.\n");
    return -1;
  }

  // %s ... ANSI
  // %S ... UNICODE
  wprintf(L"ini file path : [%S]\n", filepath);

  if (!PathFileExists(filepath))
  {
    // Not found ini file
    printf("Not found ini file. Create.\n");
    writeIni(filepath);
  }

  if (!PathFileExists(filepath))
  {
    printf("Not found ini file.\n");
    return -1;
  }

  // read ini file
  printf("Found ini file. Read.\n");
  readIni(filepath);

  // dump results
  printf("wait=%d\n", wait);
  printf("speed=%d\n", speed);
  printf("number=%d\n", number);
  printf("fps_display=%d\n", fps_display);

  return 0;
}

_Makefile
03_write_ini.exe: 03_write_ini.cpp
    g++ $< -o $@ -lshlwapi -Wall -O3 -static

.PHONY: clean
clean:
    rm -f *.exe
    rm -f *.o
    rm -f test.ini

make と打てば、03_write_ini.exe が生成される。この exeファイルを実行すると、Windows10の場合、以下のiniファイルを作成して読み書きする。
C:\Users\(USERNAME)\AppData\Roaming\dev_ini_file_rw\dev_ini_file_rw.ini

実行結果 :

実行した結果は以下。
> 03_write_ini.exe
Not found target folder. create.
ini file path : [C:\Users\USERNAME\AppData\Roaming\dev_ini_file_rw\dev_ini_file_rw.ini]
Not found ini file. Create.
Found ini file. Read.
wait=15
speed=1000
number=500
fps_display=0

> 03_write_ini.exe
ini file path : [C:\Users\USERNAME\AppData\Roaming\dev_ini_file_rw\dev_ini_file_rw.ini]
Found ini file. Read.
wait=15
speed=1000
number=500
fps_display=0

最初の実行時は、フォルダもiniファイルも無いので、フォルダの作成と、iniファイルの作成(書き込み)をしている。2度目の実行時は iniファイルがあるので、iniファイルの読み込みだけをしている。

少し解説 :

特殊フォルダの取得には、SHGetSpecialFolderPath() を使うらしい。
  • Windows限定。
  • 利用するためには、shlobj.h の include が必要。
  • _WIN32_IE の値が 0x0400 以上じゃないと有効にならないようなので、#include <shlobj.h> の前に、#define _WIN32_IE 0x0400 を書いてみた。
#define _WIN32_IE 0x0400
#include <shlobj.h>

SHGetSpecialFolderPath() に、CSIDL_* という値を渡してやることで、どの特殊フォルダを取得するのか指定できる。以下、参考ページ。

_特殊フォルダのパスを取得する
_SHGetSpecialFolderPathに設定できるCSIDL
_Windowsの特殊フォルダのパスを取得する - わびさびサンプルソース
_特殊フォルダのパス名等の取得(?)


ディレクトリのPATHと、その後に続くディレクトリ名 or ファイル名の結合には、PathCombine() を使った。利用するには shlwapi.h の include と、libshlwapi.a のリンク(-lshlwapi) が必要。

_PathCombine


ファイルやディレクトリが存在するかどうかは、PathFileExists() を使う。また、そのPATHがディレクトリかどうかは、PathIsDirectory() で調べられる。利用するためには shlwapi.h の include が必要。

_PathFileExists - Windows APIの部屋
_PathIsDirectory - 車輪のx発明 B.G's Blog


ディレクトリの作成は、CreateDirectory() を使った。これもおそらく Windows限定だろうけど…。winbase.h で定義されているけれど、windows.h の中で winbase.h を include してあるので、windows.h を include してあれば使える。

_ディレクトリ操作(Win32API)(C言語) - 超初心者向けプログラミング入門


iniファイルへの書き込みもしくは作成は、WritePrivateProfileString() を使う。また、iniファイルの読み込み(Int値の取得)は、GetPrivateProfileInt() を使う。これも Windows限定。

そんなわけで、特殊フォルダ %APPDATA% を取得して、その中にディレクトリを作って、更にその中に iniファイルを作成して読み書きできる、と分かった。

文字列を何で扱うべきかよく分からない :

C言語で実験していた時は、ファイルのPATHその他の文字列を、安易(?)に char配列に入れて処理していたけれど、C++ で試したらエラーが続出して…。どうやら wchar_t だか WCHAR だかの配列にしないといかんらしい。

ただ、関連解説ページを眺めていたら、TCHAR配列にしてる事例を多く見かけたので、今回は TCHAR で書いてみた。

しかしそのことで、printf() を使ってもファイルPATHが正しく表示されなくなってしまって…。代わりに、wprintf() や wsprintf() を使わないといけないようだなと…。かつ、wprintf("%s", filepath); ではダメで、wprintf("%S", filepath); にしないと正しく表示されなかった。"%s" は ANSI用で、"%S" は UNICODE用、らしい。

TCHAR と言うのは、状況によって、char と wchar_t のどちらかを使ってくれるものらしい。ただ、「過去のプログラムとの互換性のために残してあるので、今から作るプログラムなら使うべきではない」という話も見かけた。

_TCHAR はもう使うな - エレクトロニクス・フィーバー

そう言われても、WCHAR にすると何故かコンパイルが通らなかったりするので…。もしかすると MinGW (gcc 6.3.0) が古いのだろうか…。何にせよ、今回は TCHAR にしてしまった。これならコンパイルが通った。

各文字列も、"hoge" ではなく、L"hoge" と書いてみたり、_T("hoge") と書いてみたり…。
  • "hoge" ... char
  • L"hoge" ... LPCSTR だか LPCWSTR になる?
  • _T("hoge") ... TCHAR ?
何にせよ、_T("xxxx") としておけば、状況に合わせて char[] か wchar_t[] にしてくれるのだろう。たぶん。よく分からんけど。

このあたり、ちゃんと勉強して把握しておかないとマズイよな…。自分がC言語を勉強したり、使ってた頃って、文字列=char配列だったもので…。そこで知識が止まってるという…。

2024/01/20() [n年前の日記]

#1 [prog] C++とOpenGLで宇宙飛行的なWindows用のスクリーンセーバを作ってみた

ここ最近、C/C++ (MinGW gcc/g++ 6.3.0) と OpenGL を使って色々実験してたけど、ようやく Windwos用のスクリーンセーバを作れたので github にアップロードしておいた。昔あった、「宇宙飛行」みたいな描画をするスクリーンセーバ。

_mieki256/ssstars_opengl: C++ and OpenGL screensaver similar to ssstars.scr.


ユーザ名が日本語になってる環境でも動くことを確認できたし、サブPCに持っていっても動いてくれたので、他の環境でもおそらく動くのではないかなと…。

今回のスクリーンセーバの、プログラミング面のポイントとしては…。 ここまでできれば、後は何を描画するのか、そこだけに注力できそうかなと…。

少し解説 :

プログラム面について少し解説をメモ。

スクリーンセーバの処理は、ssstars_opengl.cpp で行ってる。

_ssstars_opengl/ssstars_opengl.cpp at main - mieki256/ssstars_opengl

C/C++ でWindows用のスクリーンセーバを作成する時は、スクリーンセーバ用のライブラリ scrnsave を利用すると作業が簡単になる。
  • ヘッダーファイルとして scrnsave.h を include する。
  • MinGW (gcc/g++ 6.3.0)なら libscrnsave.a ( or libscrnsavw.a ?) をリンクする。(-lscrnsave を gcc/g++ のオプションとしてつける。)

scrnsave を利用すると、以下の3つの関数を自分で書くだけでスクリーンセーバが作れる。
  • ScreenSaverProc() : フルスクリーン表示時の処理
  • ScreenSaverConfigureDialog() : 設定ダイアログの処理
  • RegisterDialogClasses() : 謎。

一番最後の RegisterDialogClasses() は return TRUE; だけ書いておけばいいらしいので、実質2つの関数を書くだけでいい。

フルスクリーン表示を担当する ScreenSaverProc() には、以下のメッセージが飛んでくるので、それぞれ行うべき処理を書けばいい。
  • WM_CREATE : ウインドウが生成された時にメッセージが飛んでくる。初期化時の処理を書く。タイマーもセットする。
  • WM_TIMER : タイマーで指定した一定時間毎にメッセージが飛んでくる。各フレームの描画処理を書く。描画内容が少しずつ変化するような処理を書けばアニメーションができる。
  • WM_DESTROY : ウインドウ破棄時にメッセージが飛んでくる。終了時の処理を書く。

設定ダイアログの処理を担当する ScreenSaverConfigureDialog() には、以下のメッセージが飛んでくるので、これもそれぞれ行うべき処理を書けばいい。
  • WM_INITDIALOG : 設定ダイアログが表示される際にメッセージが飛んでくる。初期化処理を書く。
  • WM_COMMAND : 設定ダイアログ上のボタン等が押された時にメッセージが飛んでくる。OKボタンが押された時、Cancelボタンが押された時の処理を書く。

設定ダイアログにどんなGUI部品を載せるか、どこに配置するかは、リソースファイル resource.rc の中に書く。
  • #include "resource.h" を最初のほうに書いておく。
  • ダイアログは、必ず、DLG_SCRNSAVECONFIGURE というIDを使って定義しておく。このIDにしておくだけで、scrnsaveライブラリが、このダイアログを設定ダイアログとして勝手に(?)表示してくれる。

リソースファイルの中で、スクリーンセーバ名も記述できる。
  • IDS_DESCRIPTION を割り当てた文字列が、スクリーンセーバ名。
  • Windowsのスクリーンセーバ設定ウインドウ上では、IDS_DESCRIPTION が割り当てられた文字列をスクリーンセーバ名として表示してくれる。

今回のプログラムの、各関数の役割は以下。
  • InitGL() : OpenGL関係の初期化処理。
  • SetupAnimation() : OpenGL関係の初期化処理と、オブジェクトワークの初期化。
  • Render() : 毎フレームの描画処理。オブジェクトの座標を変更して、OpenGLで画面描画をしている。
  • CleanupAnimation() : アニメーション関係の終了処理。ワーク等を確保して使っていたなら、ここで解放。
  • CloseGL() : OpenGL関係の終了処理。確保して使っていたアレコレを解放。
  • writeConfigToIniFile() : iniファイルに設定値を書き込み。
  • getConfigFromIniFile() : iniファイルから設定値を読み込み。


png画像を実行ファイルに含めたり、iniファイルの読み書きをするあたりは、以前のメモを参考に。

_C言語でiniファイルの読み書きをしたい - mieki256's diary
_xxdというコマンドがあることを知った - mieki256's diary
_メモリ上のpngバイナリをOpenGLのテクスチャとして利用 - mieki256's diary

参考ページ :

余談。スクリーンセーバの関連情報がどんどん消えている気がする :

思考メモ。完全に余談。

C/C++でスクリーンセーバを作る方法について色々ググって眺めてたけど、どうもこのあたり、解説ページがどんどん消滅している気がする…。WebArchive に残っていればまだ助かるけれど、残ってない場合もチラホラあって…。

「スクリーンセーバなんて今時使うやつ居ないだろ」という風潮で消えつつあるのだろうか…。ディスプレイがブラウン管から液晶に変わったことで、画面の焼き付き防止というスクリーンセーバの実用性は、たしかにもう求められていないし…。

ただ、スクリーンセーバというものは、まだ実用性があった頃から少しずつ、観賞して楽しむものに変化したジャンルで…。つまるところ、日常生活にちょっとした潤いを与えてくれる、実にさりげないCGアート作品群に変質していた。なのに「実用性」「実用性」って…。

例えば、壁にA5〜B5サイズの額縁に入った絵画がひっそり飾られていたとして。「こんなもん実用性無いだろ。無駄だ無駄」と、君は平気でソレをゴミ箱に突っ込むのかと。実用性があるものはこの世に存在する価値があって、実用性がないものは無価値だからどんどん削除していくべきだと…。0/1だけで考えるのですかと。

そう考えると、スクリーンセーバの作り方について解説してるページが少しずつ消滅している状況があるとしたら、仮にその理由が「スクリーンセーバなんて今時使うやつ居ねえだろ」という理由だったら、それは良くない流れかもしれないなと…。

とは言うものの。スクリーンセーバってインタラクティブ性があるわけでもないので、実は動画でいいんじゃないの? という気もしていて…。昔は動画をPCで再生するのは荷が重かったから、まだその都度計算してシンプルな映像をリアルタイムに提示していくほうが実現性が高かったのだろうけど。今はむしろ動画を再生しちゃうほうが確実(?)だし。

まあ、今となってはメガデモに近いところもあるのかなあ…。制約が特にないから、メガデモと比べたらなんだかゆるゆるだけど。

思考メモです。オチは無いです。

#2 [prog] HTML+JavScriptでスクリーンセーバを作れないものか

Windows用のスクリーンセーバを作りたいと思った時、C/C++で書かないといけないというのは、ちょっとシンドイよな…。こういうのって、せめて HTML + JavaScriptで書けたりしないものか…。

例えばだけど。
こんな感じのスクリーンセーバがあれば、実質的に HTML+ JavaScriptでWindows用のスクリーンセーバをいくらでも書けるのではないか、と妄想してみたりして…。

ただ、WebView2 とか Electron とかそのへん全然分からないので、そんなことができるのかどうか…。
ちなみに、HSPでやれないかと少し調べてみたけれど、HSPの場合は IEコンポーネントを表示することしかできないようだなと…。それも、ウインドウ表示ならできるけど、全画面表示ができるかどうかはちょっと分からない感じで…。そもそも表示に ActiveX を使っているようだし…。IEコンポーネントだから、昨今の HTML + JavaScript は動かないんじゃないのか、という不安も…。

2024/01/21() [n年前の日記]

#1 [basic] FreeBASICを再勉強中

スクリーンセーバの作り方についてググっていたら、FreeBASICを使ったスクリーンセーバの作成事例があるらしいと知った。「FreeBASICってそんなこともできるのか…」と気になり始めたので、少し試用してみた。

FreeBASICは、WindowsやLinuxで利用できるBASICコンパイラ。無料で使える。インタプリタではなくコンパイラなので、拡張子 .bas でソースを書いて、fbc hoge.bas で hoge.exe を生成できる。

_FreeBASIC Language | Home

今回は Windows10 x64 22H2上で使ってみた。

ちなみに、以前も少しだけ試用したことがある。

_mieki256's diary - 色々なBASICをちょっとだけ触ってみた
_mieki256's diary - FreeBASICのGUIライブラリを試用
_mieki256's diary - FreeBASICのグラフィックスライブラリを試用

インストール :

以下から辿って、.7z か .zip を入手する。

_FreeBASIC Compiler - Browse Files at SourceForge.net

今回は FreeBASIC-1.10.1-winlibs-gcc-9.3.0.7z を入手した。この版には、32bit版と64bit版の両方が入っている。

32bit版だけ欲しければ FreeBASIC-1.10.1-win32.zip を、64bit版だけ欲しければ FreeBASIC-1.10.1-win64.zip を入手すればいいのだろうけど、元々 FreeBASIC は32bit版しか無かったせいか、関連ライブラリが32bit版しかサポートしてないことも多いようで…。ここは両方入っている版を選んでおいたほうが良さそうな気がする。

解凍して任意の場所に置く。今回は D:\Dev\FreeBASIC\ に置いてみた。

ユーザ側の環境変数 PATH の最後に、D:\Dev\FreeBASIC を追加。fbc --version でバージョンを確認できたら利用できる状態になっている。
> fbc --version
FreeBASIC Compiler - Version 1.10.1 (2023-12-24), built for win32 (32bit)
Copyright (C) 2004-2023 The FreeBASIC development team.
standalone

FreeBASICでOpenGLを使う :

FreeBASIC は OpenGL を使って描画することもできる。FreeBASICインストールフォルダ\examples\graphics\OpenGL\ に、OpenGLを使ったサンプルがいくつか入っている。

fbgfx_opengl.bas のスクリーンショット。




fbgfx_texture.bas のスクリーンショット。




サンプルの中の gl_test.bas は、glut32.dll を要求する。MinGW用の freeglut.dll (freeglut 3.0.0 for MinGW、freeglut-MinGW-3.0.0-1.mp.zip) を入手して解凍。

_freeglut Windows Development Libraries
_The freeglut Project :: About

freeglut.dll を glut32.dll にリネームしつつ、.bas をビルドして生成した .exe と同じフォルダに置いたところ動いてくれた。




glext_test.bas は、glfw (libglfw.a) を要求する。以下のやり取りの中で入手先が提示されていた。

_Error when compiling - freebasic.net
_FreeBASIC Compiler - Browse /Older versions/0.90.1/Binaries - Windows/Libraries at SourceForge.net

FB-win32-glfw-2.7.7.zip を入手して解凍。FreeBASICインストールフォルダ/lib/win32/ に libglfw.a をコピーした。32bit (win32)用しか無いらしいので、試すときは 32bit版 FreeBASICを使う。この状態なら件の .bas をビルドして実行することができた。


また、以下のやり取りの中で、glfw 3.1.1 をビルドして配布されている方が居た。

_OpenGL Window library GLFW 3.1.1 (win/lin 32/64-bit) - freebasic.net

glfw-3.1.1.zip を入手して解凍すると、32bit版と64bit版の両方が入っていた。ファイル名は glfw ではなくて glfw3 になる。ただ、.bas 内の glfw.bi を glfw3.bi に変更しても、ビルドはできなかった。ちなみに元々、FreeBASIC 1.10.1 の inc/GLFW/ にも glfw3.bi が入っていた。


glu_quadrics.bas は、SDLを要求するが、-lSDL というエラーが出た。libSDL.a が必要なのだろうけど、入手方法が分からない。

試しにダメ元で、MSYS2のインストールフォルダ、D:/msys64/mingw32/lib/ から、以下のファイルを FreeBASICインストールフォルダ/lib/win32/ にコピーしてみたところ、件の .bas もビルドして動作させることができた。
libSDL.a
libSDL_gfx.a
libSDL_image.a
libSDL_mixer.a
libSDL_net.a
libSDL_ttf.a

こういうやり方で本当にいいのか分からんけど…。実際に動いてるから、まあ、いいか…。




せっかくの機会(?)なので、MSYS2 上で以下のパッケージもインストールしておいた。pacman -S xxxx でインストールできる。
mingw-w64-i686-SDL
mingw-w64-i686-SDL_image
mingw-w64-i686-SDL_gfx
mingw-w64-i686-SDL_net
mingw-w64-i686-SDL_ttf
mingw-w64-i686-SDL_mixer

mingw-w64-x86_64-SDL
mingw-w64-x86_64-SDL_gfx
mingw-w64-x86_64-SDL_net
mingw-w64-x86_64-SDL_ttf
mingw-w64-x86_64-SDL_image
mingw-w64-x86_64-SDL_mixer

mingw-w64-i686-SDL2
mingw-w64-i686-SDL2_gfx
mingw-w64-i686-SDL2_net
mingw-w64-i686-SDL2_ttf
mingw-w64-i686-SDL2_image
mingw-w64-i686-SDL2_mixer
mingw-w64-i686-SDL2_sound

mingw-w64-x86_64-SDL2
mingw-w64-x86_64-SDL2_gfx
mingw-w64-x86_64-SDL2_net
mingw-w64-x86_64-SDL2_ttf
mingw-w64-x86_64-SDL2_image
mingw-w64-x86_64-SDL2_mixer
mingw-w64-x86_64-SDL2_sound

IDE FbEditをインストール :

FreeBASICのソースを書くためのIDEっぽいアプリもインストールしてみた。一つは FbEdit。

_FbEdit

上記のページから FbEdit 1.0.7.4 Unicode対応版、fbedit1.0.7.4Unicode.zip を入手して解凍。今回は D:\Prog\FbEdit\ にインストール。

FbEdit.exe を実行すると、FreeBASICコンパイラ(fbc.exe)がある場所その他を尋ねてくるので設定。

少し触ってみたけれど、リソースエディタというか、フォームデザイナーも入っているのだな…。リソースファイルを選択して、ダイアログのレイアウトをGUI操作で作っていける。

IDE WinFBEをインストール :

WinFBE というIDEもインストールしてみた。

_WinFBE Editor and FreeBASIC Compiler (All-in-One Package) (V3.1.0 June 4, 2023) - freebasic.net
_PaulSquires/WinFBE: FreeBASIC Editor for Windows
_Releases - PaulSquires/WinFBE

WinFBE_Suite.zip (3.1.0) を入手して解凍。D:\Prog\WinFBE\ にインストールした。

これもフォームデザイナーがついている。FbEdit にしろ、WinFBE にしろ、ちょっとした GUIアプリなら作れる機能を用意しているようだなと…。

FBImageをインストール :

FreeBASICは標準状態だと bmp画像ファイルしか開けないようだけど、jpg, png, tga などを開いたり保存したりできる FBImage というライブラリがあるらしい。せっかくだからインストールしてみた。

_FBImage static Win/Lin 32/64-bit - freebasic.net

FBImage.zip を入手して解凍。中には以下が入っている。
FBImage.bi
lib/lin/libFBImage-32-static.a
lib/lin/libFBImage-64-static.a
lib/win/libFBImage-32-static.a
lib/win/libFBImage-64-static.a
test_*.bas

Windows環境の場合、以下のファイルを FreeBASICインストールフォルダ以下にコピー。
  • FBImage.bi を、FreeBASICインストールフォルダ/inc/ にコピー。
  • lib/win/libFBImage-32-static.a を、FreeBASICインストールフォルダ/lib/win32/ 以下にコピー。
  • lib/win/libFBImage-64-static.a を、FreeBASICインストールフォルダ/lib/win64/ 以下にコピー。

解凍したフォルダの中にはサンプルファイルが入っている。 test_*.bas がサンプルファイル。それぞれ数行で画像の読み込みや作成ができるようになっていた。一通りビルドして動かしてみたけれど、たしかに画像の読み書きができた。

2024/01/22(月) [n年前の日記]

#1 [basic] FreeBASICで画像描画

Windows10 x64 22H2 + FreeBASIC 1.10.1 で、FreeBASIC について勉強中。

まずは画像を表示できないことにはそれらしいプログラムも書けないだろうと思えたので、画像ファイルの読み込みと描画について少し実験。

インストールの仕方は以下を参考に。

_FreeBASICを再勉強中 - mieki256's diary

bmp画像の読み込みと描画 :

FreeBASICは標準状態で、bmp画像の読み込みに対応しているらしい。ひとまずサンプル(examples/manual/gfx/bload2.bas)を参考にして動作確認してみた。

_bload.bas
' bload ... bmp image Load

' カレントディレクトリを exeファイルのある場所にする
chdir exepath()

' ウインドウサイズと色深度を指定
screenres 512, 288, 32

' 画像格納用の領域を確保
Dim img As any ptr = ImageCreate(256, 256)

' bmp画像読み込み
bload "image.bmp", img

' 画像を描画。RGB=(255, 0, 255) のピクセルは透明色として扱う
Put (16, 16), img, TRANS

' 画像を使い終わったので破棄
ImageDestroy(img)

' キー入力があるまで待つ
Sleep

使用するbmp画像(256x256、bpp 32bit)は、以下のzipに入れておいた。

_image.zip

以下はpng画像だけど、今回はこんな感じの画像を読み込んで描画しようとしている。

image_png.png


fbc bload.bas と打てば bload.exe を生成することができる。実行すると以下のようなウインドウが表示される。bmp画像を読み込んで描画することができている。

bload_ss.png


注意点。FreeBASIC は、画像の中の、RGB=(255,0,255) を ―― 要はピンク? 紫? を透明色にして描画することができるらしい。実際、上記の動作結果でも、四隅が透明色扱いになって描画されている。

png画像の読み込みと描画 :

FBImageというライブラリを利用すれば、FreeBASIC でpng、jpg、tga を読み込むこともできる。ライブラリの入手先は以下。

_FBImage static Win/Lin 32/64-bit - freebasic.net

FBImage.zip を入手して解凍。
  • .bi ファイルは FreeBASICインストールフォルダ/inc にコピー。
  • .a ファイルは FreeBASICインストールフォルダ/lib/win??/ 以下にコピー。
これで FBImage をインストールできる。

サンプルソースは以下。

_fbimage.bas
' load RGB png image with FBImage

' 画像読み込み用ライブラリ FBImage を使う
#include once "FBImage.bi"

' カレントディレクトリを exeファイルのある場所にする
chdir exepath()

' デスクトップ解像度を指定
Dim As Integer scrw = 512
Dim As Integer scrh = 288

' ウインドウサイズと色深度(bpp)を指定
screenres scrw, scrh, 32

' 画像読み込み
var img = LoadRGBAFile("image_png.png")

' 画像を描画。RGB=(255, 0, 255) のピクセルは透明色として扱う
Put (16, 16), img, TRANS

' 画像を使い終わったので破棄
ImageDestroy img

' キー入力があるまで待ち続ける
sleep

  • FBImage を使うには、#include once "FBImage.bi" を最初のあたりに記述する。
  • LoadRGBAFile() で画像を読み込める。
それ以外は、bmp画像の読み込みと似た感じで処理できる。

fbc fbimage.bas と打てば fbimage.exe を生成できる。実行すると以下のようなウインドウになった。png画像を読み込んで描画することができている。

fbimage_ss.png

フルスクリーン表示 :

せっかくだからフルスクリーン表示っぽいことができるかどうか試してみる。

FreeBASICは、ウインドウ表示モードの他に、フルスクリーン表示モードも指定できるようだけど、今回はウインドウ表示モードを利用して、ウインドウサイズをデスクトップ解像度と同じにした。更に、ウインドウのフレーム(境界線)を表示しないようにフラグを設定してやる。これで見た目はフルスクリーン表示のように見えるはず。

_fbimage_fullscreen.bas
' load RGB png image with FBImage
' full screen, window mode

' fbgfxモードを使う
#Include "fbgfx.bi"
Using fb

' 画像読み込み用ライブラリ FBImage を使う
#include once "FBImage.bi"

' カレントディレクトリを exeファイルのある場所にする
chdir exepath()

' デスクトップ解像度を取得
Dim As Integer scrw, scrh
ScreenInfo(scrw, scrh)

' ウインドウサイズと色深度(bpp)を指定。ウインドウの境界線(フレーム)は無し
screenres scrw, scrh, 32, , GFX_NO_FRAME

' 画像読み込み
var img = LoadRGBAFile("image_png.png")

' 画面クリア
color RGB(255, 255, 255), RGB(40, 80, 160)
cls

' マウスカーソル非表示
SetMouse ,, 0

' 画像を描画。RGB=(255, 0, 255) のピクセルは透明色として扱う
Put ((scrw - 256) / 2, (scrh - 256) / 2), img, TRANS

' 画像を使い終わったので破棄
ImageDestroy img

' キー入力があるまで待ち続ける
sleep

' マウスカーソル表示
SetMouse ,, 1

fbc fbimage_fullscreen.bas と打って、fbimage_fullscreen.exe を生成して実行。フルスクリーン表示っぽい見た目になった。

#2 [anime] 「勇気爆発バーンブレイバーン」2話を視聴

録画してたソレを視聴。ロボットアニメ。

いやはや、なんというか…これは…。とにかくヤバイな…。かなりヤバイロボットアニメが始まってしまった気がする…。ヤバイ"ロボットアニメ"じゃなくて"ヤバイロボット"アニメなあたりがなんとも…。素晴らしい。

ロボットに意思が有るか無いか :

元々日本の漫画に登場する巨大ロボットって意思を持っているのが当たり前だった気がするのだけど。マジンガーZあたりで乗り物としての巨大ロボットが人気を博して、そこからは乗り物的な巨大ロボットアニメが席巻しちゃったわけで。しかし、件のアニメを見ると、ロボットにも大なり小なり何かしらの意思があったほうが面白くなるよなあ、と…。メカじゃなくてキャラクターになるんだよな…。

勇者ロボシリーズのように人間に近い意志があってもいいし、レイズナーやイデオンのようにメカっぽい意思でも良い…。ロボットであれば、人間とはまた違った価値観や考え方も自然に提示することができて美味しそうな気がする。異文化衝突とでも言うか…。ロボットにとっての常識は人間にとって非常識だろうし、人間にとっての常識はロボットにとって非常識だろうし…。まあ、それは宇宙人でも妖精でも何でも当てはめて提示できるだろうけど。

それにしても、件のアニメは…。まさかこんな関係性を描ける余地が残っていたとは…。一体何を食べたらこんな発想ができるんだ…。素晴らしい。

2024/01/23(火) [n年前の日記]

#1 [basic] FreeBASICでゲームのメインループ相当を書きたい

Windows10 x64 22H2 + FreeBASIC 1.10.1 で実験中。

FreeBASIC で、リアルタイムゲームのメインループっぽいものを書きたい。

リアルタイムゲームを作る場合は、例えば1秒間に60回ほど処理される無限ループのようなもの(メインループ)を書いて、その中に1フレーム分の処理を書いたりする。

そのためには、1ループにつき、1.0秒 / 60回 ≒ 0.016666666667 sec(秒) ≒ 16.67 msec(ミリ秒) ぐらいで処理してくれる作りにしないといけない。大体は、ゲームの内部処理のほうが早く処理を終えてしまうので、ループの最後のほうで 16.67 msec が経過するように時間待ちの処理を入れることになるのだけど…。

about_mainloop.png

こういった処理を書くためには、そのプログラミング言語に、以下の機能が必要になる。
ということで、FreeBASIC で経過時間を調べたり、時間待ちをする機能があるのかどうかを調べた。

時間の取得と時間待ち :

時間の取得には、Timer という関数が使える。

_Timer

特定の時間からの経過時間を秒(sec)で返す。小数点以下の値も返してくるので、ミリ秒(msec)やマイクロ秒も測定できる。もっとも、精度についてはどこまで正確なのか分からんけど…。

Windowsの場合はPCが起動してからの経過時間を返してくるようだけど、プラットフォームによっては真夜中からの時間を返してくるものもある、と書いてある…。前フレームの測定時間が、現在フレームの測定時間より過去にあるとは限らない、という前提で処理を書いておかないといかんのだろうな…。


時間待ちについては、Sleep という関数が使える。

_Sleep

ミリ秒(msec)単位で待ち時間を指定する模様。

ちなみに、値を指定しないで呼ぶと、キー入力があるまで待つ、という処理になるらしい。

ループを書いてみた :

そんなわけで、Timer と Sleep を使って、メインループっぽいものを書いてみた。1秒間ループさせてみて、何回処理が通ったかをカウントする。また、ループ1回あたりに何秒かかっているかを出力する。

_timer.bas
Dim As Double start_time, prev_time, now_time, diff_time
Dim As Integer frame_count

frame_count = 0
start_time = Timer
prev_time = start_time

' 1秒間ループする
Do
    now_time = Timer
    diff_time = now_time - prev_time
    prev_time = now_time
    Print "Diff: "; diff_time
    
    sleep 10.0
    frame_count += 1
Loop until (now_time - start_time) > 1.0

Print "frame_count=" & frame_count

Print "Push Any Key"

sleep

fbc timer.bas と打てば、timer.exe ができる。実行してみる。

> timer.exe
Diff:  1.00000761449337e-007
Diff:  0.01270929999918735
...
Diff:  0.01561659999970289
Diff:  0.01563919999898644
frame_count=62
Push Any Key

一見それらしく動いてるようだけど、実はおかしい。ループの最後で Sleep を使って、問答無用で 10.0ミリ秒時間待ちせよ、と書いているのだから、1秒間処理させたらフレームカウントは100前後になるはずでは…?

このあたり、各OSのタイマー精度が関係してる。Windows10 の場合、15msec程度の精度しかないらしい。 *1 だから、Slepp 10.0 を指定しても、15msec ぐらい Sleep していて、こういう結果になる模様。これがLinuxなら1msecの精度があるらしいのだけど…。

_Timer (Win32 and Win64) - freebasic.net
_Speed - freebasic.net
_Constant Framerate - freebasic.net

※ 2024/01/24追記。Ubuntu Linux 20.04 LTS上で上記のサンプルを動かしてみたところ、フレームカウントが 96 になった。100前後の値が出ているので、Sleep はちゃんとミリ秒単位で動いてくれている模様。

改良版のループを書いてみた :

前述の問題の解決策として、一時的にタイマー精度を1msecに細かくしてしまうことができるらしい。マルチメディア関係の機能を使うのだとか。この時点で Windows に特化したプログラムになってしまうけど、背に腹は代えられない…。
  • timeBeginPeriod(1) ... タイマー精度を1msecに向上させる
  • timeEndPeriod(1) ... タイマー精度を本来のスペックに戻す。

FreeBASIC で上記の機能を使うためには、以下のヘッダーファイルを include しておく必要がある。
#include "windows.bi"
#include "win/mmsystem.bi"

また、必要な処理が終わったら、元の精度に戻しておかないといけない。この点はちょっと忘れがちなので注意。

そんなわけで、手直ししたのが以下。

_timer2.bas
' mmsystem(マルチメディア関連ライブラリ)を利用して、
' Windows上のタイマー精度を一時的に 1msec に向上させる。
' 処理が終わったら本来のタイマー精度に戻すこと。

' mmsystemを利用
#include "windows.bi"
#include "win/mmsystem.bi"

Dim As Double start_time, prev_time, now_time, diff_time, one_frame
Dim frame_count As Integer 

' タイマー精度を1msecに向上
timeBeginPeriod(1)

' 1フレームあたりの本来の時間
one_frame = 1.0 / 60.0

frame_count = 0
start_time = Timer
prev_time = start_time

' 1秒間ループさせる
Do
    now_time = Timer
    diff_time = now_time - prev_time
    prev_time = now_time
    Print "Diff: "; diff_time

    If Timer < (now_time + one_frame) Then
        ' 本来のフレーム時間がまだ経過してないので sleep させる
        sleep ((now_time + one_frame) - Timer) * 1000.0
    End If
    frame_count += 1
Loop until (now_time - start_time) >= 1.0

' タイマー精度を本来のスペックに戻す
timeEndPeriod(1)

Print "frame_count=" & frame_count

Print "Push Any Key"

sleep

fbc timer2.bas と打てば、timer2.exe が生成される。実行してみる。

> timer2.exe
Diff:  1.00000761449337e-007
Diff:  0.01655399999981455
Diff:  0.01653780000015104
...
Diff:  0.01651980000133335
Diff:  0.01651969999875291
Diff:  0.01650789999985136
Diff:  0.01652210000065679
frame_count=62
Push Any Key

16msec前後でループが回っているように見える。これでどうにかなりそう。

ゲームのメインループっぽい処理を書いてみる :

画像を何かしら描画して、ゲームのメインループっぽい感じに見える処理を書いてみる。

png画像の読み込みには FBImage というライブラリを使った。導入の仕方は、昨日の日記にメモしてある。

_FreeBASICで画像描画 - mieki256's diary

さておき、サンプルソースは以下。

_mainloop.bas
' mmsystemを利用
#include "windows.bi"
#include "win/mmsystem.bi"

' fbgfxモードを使う
#Include "fbgfx.bi"
Using fb

' 画像読み込み用ライブラリ FBImage を使う
#include once "FBImage.bi"

' 円周率を定義
Const PI As Double = 3.1415926535897932

Dim As Double start_time, prev_time, now_time, diff_time, one_frame
Dim As Integer frame_count
Dim As String fps_text = "FPS"
Dim As Integer scrw, scrh, imgw, imgh

' カレントディレクトリを exeファイルのある場所にする
chdir exepath()

' ウインドウサイズと色深度を指定
scrw = 1280
scrh = 720
Screenres scrw, scrh, 32

' 画像読み込み
var img = LoadRGBAFile("image_circle_96x96.png")

' 画像の幅と高さを取得
imageinfo img, imgw, imgh

' タイマー精度を1msecに向上
timeBeginPeriod(1)

' 1フレームあたりの本来の時間
Dim As Double MAX_FPS = 60.0
one_frame = 1.0 / MAX_FPS

start_time = Timer
prev_time = start_time
frame_count = 0

Dim As Boolean running = True
Dim As Double angle = 0.0

' メインループ
While (running)

    ' 前回のフレームから何秒経過しているのかを取得。単位は秒(小数点以下有り)
    now_time = Timer
    If now_time >= prev_time Then
        diff_time = now_time - prev_time
    Else
        diff_time = one_frame
    End If
    prev_time = now_time

    If now_time >= start_time Then
        If (now_time - start_time) >= 1.0 Then
            ' FPSを取得
            fps_text = "FPS: " & frame_count
            start_time += 1.0
            frame_count = 0
        End If
    Else
        start_time = now_time
    End If

    If inkey() <> "" Then
        ' 何かのキーが押されたのでループ終了
        running = False
    End If

    angle += (1.0 * MAX_FPS) * diff_time

    ' 描画開始
    ScreenLock

    ' 画面クリア
    color RGB(255, 255, 255), RGB(30, 60, 120)
    cls

    ' 画像群を描画
    Dim As Integer x, y
    For i As Integer = 0 To 48
        Dim As Double ang = (angle + (i * 5.0)) * PI / 180.0
        x = (scrh * 0.4) * Cos(ang) + (scrw / 2) - (imgw / 2)
        y = (scrh * 0.4) * sin(ang) + (scrh / 2) - (imgh / 2)

        Put (x, y), img, TRANS  ' 画像を描画
    Next i

    ' 文字列を描画
    Draw String (10, 10), fps_text

    ' 描画終了
    ScreenUnlock

    If Timer < (now_time + one_frame) Then
        ' 本来のフレーム時間がまだ経過してないので sleep させる
        sleep ((now_time + one_frame) - Timer) * 1000.0
    End If

    frame_count += 1
Wend

' タイマー精度を本来のスペックに戻す
timeEndPeriod(1)

' 画像を使い終わったので破棄
ImageDestroy img

' 最後まで処理が来たのか確認するためにメッセージボックスを表示してみる
' MB_TOPMOST Or MB_SETFOREGROUND を指定しないと、メインウインドウの後ろに隠れてしまう
' MessageBox(NULL, "Cleanup", "Message", MB_OK Or MB_TOPMOST Or MB_SETFOREGROUND)

使用画像ファイルは以下。

_image_circle_96x96.png

fbc mainloop.bas と打てば、mainloop.exe が生成される。実行結果は以下。




一応、60FPS前後で動いてくれた。なんだか動きがガクガクしているような気もするけれど…。でもまあ、一応それっぽい処理を書けそうではあるかな…。

ダブルバッファについて :

ウインドウ内に色々描画する際にチラついたら嫌なのでダブルバッファ処理をしてみたいと思ったのだけど。ググってみたら FreeBASIC は、 fbgfx なるソレを使っている場合、デフォルトでダブルバッファに対応しているそうで。

描画開始時に ScreenLock を呼んで、描画終了時に ScreenUnlock を呼べば、ダブルバッファになってくれるよ、という話を目にした。

_Double Buffered window? - freebasic.net
_Double buffer for grpahics. - freebasic.net

ちなみに、OpenGL で描画してる場合は、flip を呼ぶことでダブルバッファの切り替えになるらしい。

ウインドウの閉じるボタンを押したらどうなるのか :

ところで、このプログラムは、何かのキーを押したら終了するように作ってあるのだけど。ウインドウの右上の閉じるボタンを押したときは、プログラムの最後のあたりの終了処理を ―― 画像を破棄したり、タイマー精度を元に戻したりするあたりを通ってくれるのだろうか、という不安が湧いてきた。

ググったところ、FreeBASIC においては、ウインドウの閉じるボタンを押した際、特定のキー入力として入ってくるようになっているらしい。

_The close button in windowed-mode - freebasic.net
_Windows 10 Window Close - freebasic.net
_Close window - freebasic.net

chr(255) & "k"、もしくは、chr(255, 107)、あるいはもしかすると、chr(0) & "k" が Inkey() で検知された場合、それはウインドウの閉じるボタンが押された状態だよ、ということになっているそうで。

実際、前述プログラムの一番最後の行、MessageBox の行のコメントアウトを外して動作確認したところ、ウインドウの閉じるボタンを押した時も MessageBox が表示された。

メインループを抜ける条件の一つとして、件のキーが入ったかどうかをチェックするようにしておけば、問題は無さそうだなと…。

*1: Windows の種類・バージョンによって、精度は異なる。

2024/01/24(水) [n年前の日記]

#1 [basic] FreeBASICをLinux上で使ってみた

FreeBASIC 1.10.1 を、Ubuntu Linux 20.04 LTS (x64) 上でも使ってみた。

_FreeBASIC Language | Home

FreeBASIC は、無料で利用できるBASICコンパイラ。WindowsやLinuxで使うことができる。BASICでソースを書いてコンパイルすると単一の実行形式ファイルになる。GUIアプリも作れる。構造体やクラスも使える。

FreeBASICの古い版をアンインストール :

自分の環境では、FreeBASIC 1.07.3 が既にインストールされていたので、一旦アンインストールした。

FreeBASIC-1.07.3-ubuntu-20.04-x86_64.tar.gz を展開したディレクトリの中に入って以下を打つ。-u をつけることでアンインストールできるらしい。
sudo ./install.sh -u

これで、以下のディレクトリに入っていた FreeBASIC がアンインストールされる。
/usr/local/bin/fbc
/usr/local/include/freebasic/
/usr/local/lib/freebasic/

現行版をインストール :

まずは FreeBASIC の動作に必要なパッケージを apt でインストールしておく。
sudo apt install gcc libc6-dev ncurses-dev libx11-dev libxext-dev libxpm-dev libxrandr-dev libxrender-dev

続いて、FreeBASIC 1.10.1 を入手。

_FreeBASIC Compiler - Browse /FreeBASIC-1.10.1/Binaries-Linux at SourceForge.net
_FreeBASIC Compiler - Browse /FreeBASIC-1.10.1 at SourceForge.net

今回は FreeBASIC-1.10.1-linux-x86_64.tar.gz を入手した。何故か Ubuntu Linux 20.04 用が無いので…。22.04 や 18.04用はあるんだけどなあ…。

展開して、ディレクトリに入って以下を打つ。
sudo ./install.sh -i

これで、以下のディレクトリに FreeBASIC 1.10.1 がインストールされた。
/usr/local/bin/fbc
/usr/local/include/freebasic/
/usr/local/lib/freebasic/

fbc のバージョンを確認。
$ fbc -version
FreeBASIC Compiler - Version 1.10.1 (2023-12-24), built for linux-x86_64 (64bit)
Copyright (C) 2004-2023 The FreeBASIC development team.

FBImageライブラリを使用 :

FreeBASICで、png,jpg,tga 画像を読み込めるようにするライブラリ、FBImage が使えるのか試してみた。

_FBImage static Win/Lin 32/64-bit - freebasic.net

FBImage.zip を解凍すると、lib/lin/ の中に、Linux用のライブラリファイル(.a)も入っている。

例えば、Ubuntu Linux 20.04 LTS上で FreeBASICをインストールした場合、以下の場所にヘッダーファイル(.bi)やライブラリファイル(.a)が入るので…。

/usr/local/include/freebasic/
/usr/local/lib/freebasic/linux-x86_64/

FBImage の、.bi や .a を、上記のフォルダにコピーしてやればいい。
sudo cp *.bi /usr/local/include/freebasic/
sudo cp *.a /usr/local/lib/freebasic/linux-x86_64/

ただ、.bas ファイルと同じ場所に、.bi や .a を置いておくだけでも、コンパイルは通るようだなと…。システムフォルダ(?)にコピーせずに、プロジェクトフォルダ(.basと同じ場所)に置きっぱなしにして使ったほうがいいのかもしれない。

さておき。
  • FreeBASIC 1.07.3 で FBImage を使おうとしたら .bas のビルド時にエラーが出てしまった。
  • FreeBASIC 1.10.1 で FBImage を使ったら .bas もコンパイルできたし、実行ファイルも動いてくれた。
FreeBASIC も FBImage も、現行版を使ったほうが良さそうだなと…。

ソースを1つにしたい :

Windows と Linux の両方で使えるソースにしたい。ソースの中で、「ここからここまでは Windows用」「ここからここまでは Linux用」と振り分けることはできないのだろうか。

以下の定義が使えるらしい。
__FB_WIN32__
__FB_LINUX__

_FB_WIN32
_FB_LINUX

Windowsだけが使う処理を書くときは、以下のように書く。
#ifdef __FB_WIN32__

' mmsystemを利用
#include "windows.bi"
#include "win/mmsystem.bi"

' タイマー精度を1msecに向上
timeBeginPeriod(1)

#endif

Linuxだけが使う処理を書くときは、以下のように書く。
#ifdef __FB_LINUX__

' Linux only

#endif

#2 [basic] FreeBASICでlibpngを使おうとした

Windows10 x64 22H2 + FreeBASIC 1.10.1 で、libpng が使えるのかどうか気になった。libpng というのは、png画像の読み書きをするためのライブラリ。

FreeBASICインストールフォルダ内の、examples\files\libpng\ の中に、png.bas と言うファイルがある。これが libpng を利用するサンプルソース。png.bi を include することで libpng 関連の機能が利用できるらしい。

しかし、fbc png.bas と打って exe を作ろうとしたところ、-lpng でエラーが出た。ライブラリファイル libpng.a が無いというか、見当たらないっぽい。

MSYS2のファイルを流用しようとしたけど失敗 :

MSYS2 の中に入っている libpng.a を探してコピーしようとしたけれど、なんだか色んなファイルがあって…。MSYS2インストールフォルダ\mingw32\lib\ の中に、ファイルはあるようだけど…。
libpng.a
libpng.dll.a
libpng16.a
libpng16.dll.a

一般的に、*.dll.a は、DLLファイルがあることを前提にしたライブラリファイルだったはず。動的リンク用。

できれば静的リンクをしたいので、*.dll.a ではないファイルを、FreeBASICインストールフォルダ\lib\win32\ にコピーしてみた。

しかし、fbc png.bas と打つと、大量にエラーが出る。

であればと、*.dll.a をコピーしてみた。これならビルドが成功して png.exe が生成された。.dll も必要になるはずなので、MSYS2インストールフォルダ\mingw32\bin\ の中から、libpng16-16.dll もコピーしてきた。しかし、実行してみると「エントリーポイントが無い」云々のエラーが出る。

libzとlibpngをビルドしてみた :

色々試してみたけどエラーが出る。何をどうしたらいいのかさっぱり分からない。

仕方ないので、MinGW (gcc 6.3.0) を使って、zlib と libpng をビルドしてみた。ビルドに使ったソースは以下。
  • zlib-1.3.1.tar.gz
  • libpng-1.6.40.tar.xz

zlib-1.3.1.tar.gz を解凍して、ディレクトリの中に入って、以下を打ってビルド。
make -f win32/Makefile.gcc all

以下のファイルが生成された。
libz.a
libz.dll.a
zconf.h
zlib.h
zlib1.dll

libpng-1.6.40.tar.xz を解凍して、ディレクトリの中に入って、以下を打ってビルド。
make -f scripts/makefile.gcc

以下のファイルが生成された。
libpng.a
png.h
pngconf.h
pngdebug.h
pnginfo.h
pnglibconf.h
pngpriv.h
pngstruct.h

こうして出来上がったファイルの中から、libz.a と libpng.a の2ファイルだけを、FreeBASICインストールフォルダ\lib\win32\ にコピーしてみた。また、libz.a だけを使ってほしいので、libz.dll.a は libz.dll.a.esc に、libzlib.a は libzlib.a.esc にリネームして使われないようにしてみた。

この状態なら、fbc png.bas でビルドが通って、png.exe が生成された。また、png.exe を実行してもエラーにならない。ちゃんと png画像が表示された。

同じ手順で、MSYS2 MINGW64 (gcc 13.2.0) を使って、64bit版の libz.a と libpng.a をビルドしてみた。コレを、FreeBASICインストールフォルダ\lib\win64\ 以下にコピー。fbc64 png.bas と打ってみたら、32bit版と同様、png.exe が生成された。かつ、実行してもエラーにならなかった。

そんなわけで、zlib と libpng を、MinGW(gcc 6.3.0)、もしくは MSYS2 MINGW64 (gcc 13.2.0) を用いて自分でビルドして *.a を作って、その *.a を FreeBASIC で利用すれば、libpng を使ったサンプルソースもビルドできるし動いてくれる、と分かった。

せっかくビルドしたので一応置いときます。*_mingw が32bit版で、*_mingw64 が64bit版。他の環境で使えるのかどうかは分からんけど…。

_zlib-1.3.1_build_mingw.zip
_libpng-1.6.40_build_mingw.zip

_zlib-1.3.1_build_mingw64.zip
_libpng-1.6.40_build_mingw64.zip

2024/01/25(木) [n年前の日記]

#1 [prog] OSDN版のMinGWについて調べてた

OSDN(osdn.net)の閉鎖が決まったという話を見かけた。

_老舗掲示板サイト「スラド」が2024年1月末に終了、「OSDN」も接続困難なまま閉鎖か - 窓の杜

困った。ここ最近利用している MinGW (C/C++コンパイラ + 開発ツール一式) の最新版は OSDN で配布されているわけで…。今後は MinGW が手に入らなくなってしまう…。アプリのビルド等の実験ができなくなる…。

いやまあ、まだ SourceForge から入手できなくはないのだけど、ただ、色々なパッケージのバージョンが古いのだよな…。

_MinGW - Minimalist GNU for Windows download | SourceForge.net

実際のところ OSDN版 MinGW は、SourceForge版 MinGW と比べて、どのくらいパッケージが差し変わっているのか気になってきた。そんなわけで、少し調べてみた。

全パッケージをインストールしてみた :

とりあえず、SourceForge版、OSDN版、両方の MinGW で、ほぼ全パッケージをダウンロードしてインストールしてみた。インストール時に、なんだか大量にコンフリクト(衝突)が起きていたのが気になるけど…。でもまあ、パッケージファイルのダウンロードぐらいはできてるだろう…。

ダウンロードされたパッケージは、以下の場所にキャッシュとして残っている。
MinGWインストールフォルダ\var\cache\mingw-get\packages\

SourceForge版と、OSDN版の、それぞれのキャッシュの状態を比較すれば、どのパッケージが差し変わった/新しくなったのかが分かるのではないかなあ、と…。

差し変わったのかもしれないファイルの一覧 :

ということで、OSDN版のキャッシュにはあるけど、SourceForge版のキャッシュには無いパッケージファイルは、以下のような状態だった。

binutils-2.32-1-mingw32-bin.tar.xz
binutils-2.32-1-mingw32-dev.tar.xz
binutils-2.32-1-mingw32-info.tar.xz
binutils-2.32-1-mingw32-lang.tar.xz
binutils-2.32-1-mingw32-man.tar.xz
dos2unix-7.3.2-1-mingw32-bin.tar.lzma
dos2unix-7.3.2-1-mingw32-doc.tar.lzma
dos2unix-7.3.2-1-mingw32-lic.tar.lzma
expat-2.2.9-1-mingw32-bin.tar.xz
expat-2.2.9-1-mingw32-html.tar.xz
expat-2.2.9-1-mingw32-lic.tar.xz
expat-2.2.9-1-mingw32-man.tar.xz
gcc-9.2.0-2-mingw32-info.tar.xz
gcc-9.2.0-2-mingw32-lang.tar.xz
gcc-9.2.0-2-mingw32-lic.tar.xz
gcc-9.2.0-2-mingw32-man.tar.xz
gcc-ada-9.2.0-2-mingw32-bin.tar.xz
gcc-c++-9.2.0-2-mingw32-bin.tar.xz
gcc-c++-9.2.0-2-mingw32-man.tar.xz
gcc-core-9.2.0-2-mingw32-bin.tar.xz
gcc-fortran-9.2.0-2-mingw32-bin.tar.xz
gcc-fortran-9.2.0-2-mingw32-info.tar.xz
gcc-fortran-9.2.0-2-mingw32-man.tar.xz
gcc-objc-9.2.0-2-mingw32-bin.tar.xz
gmp-6.1.2-3-mingw32-dev.tar.xz
gmp-6.1.2-3-mingw32-info.tar.xz
gmp-6.1.2-3-mingw32-lic.tar.xz
isl-0.21-2-mingw32-dev.tar.xz
isl-0.21-2-mingw32-doc.tar.xz
isl-0.21-2-mingw32-lic.tar.xz
libatomic-9.2.0-2-mingw32-dll-1.tar.xz
libcharset-1.14-4-mingw32-dll-1.tar.xz
libexpat-2.2.9-1-mingw32-dev.tar.xz
libexpat-2.2.9-1-mingw32-dll-1.tar.xz
libgcc-9.2.0-2-mingw32-dll-1.tar.xz
libgfortran-9.2.0-2-mingw32-dll-5.tar.xz
libgmp-6.1.2-3-mingw32-dll-10.tar.xz
libgmpxx-6.1.2-3-mingw32-dll-4.tar.xz
libgnat-9.2.0-2-mingw32-dll-9.tar.xz
libgomp-9.2.0-2-mingw32-dll-1.tar.xz
libiconv-1.14-4-mingw32-bin.tar.xz
libiconv-1.14-4-mingw32-dev.tar.xz
libiconv-1.14-4-mingw32-dll-2.tar.xz
libiconv-1.14-4-mingw32-html.tar.xz
libiconv-1.14-4-mingw32-lang.tar.xz
libiconv-1.14-4-mingw32-lic.tar.xz
libiconv-1.14-4-mingw32-man.tar.xz
libisl-0.21-2-mingw32-dll-21.tar.xz
liblzma-4.999.9beta_20100401-1-msys-1.0.13-dll-1.tar.gz
liblzma-5.2.5-2-mingw32-dev.tar.xz
liblzma-5.2.5-2-mingw32-dll-5.tar.xz
libmingwex-5.4.2-mingw32-dev.tar.xz
libmingwex-5.4.2-mingw32-dll-4.tar.xz
libmpc-1.1.0-1-mingw32-dll-3.tar.xz
libmpfr-4.0.2-1-mingw32-dll-6.tar.xz
libncurses-6.2-2-mingw32-dev.tar.xz
libncurses-6.2-2-mingw32-dll-6.tar.xz
libobjc-9.2.0-2-mingw32-dll-4.tar.xz
libpthread-2.8.0-3-mingw32-dll-2.tar.lzma
libquadmath-9.2.0-2-mingw32-dll-0.tar.xz
libquadmath-9.2.0-2-mingw32-info.tar.xz
libssp-9.2.0-2-mingw32-dll-0.tar.xz
libstdc++-9.2.0-2-mingw32-dll-6.tar.xz
libz-1.2.11-1-mingw32-dev.tar.xz
libz-1.2.11-1-mingw32-dll-1.tar.xz
mingw-get-0.6.3-mingw32-pre-20170905-1-bin.tar.xz
mingw-get-0.6.3-mingw32-pre-20170905-1-gui.tar.xz
mingw-get-0.6.3-mingw32-pre-20170905-1-lic.tar.xz
mingw-get-setup-0.6.3-mingw32-pre-20170905-1-dll.tar.xz
mingw-get-setup-0.6.3-mingw32-pre-20170905-1-xml.tar.xz
mingwrt-5.4.2-mingw32-dev.tar.xz
mingwrt-5.4.2-mingw32-dll.tar.xz
mingwrt-5.4.2-mingw32-lic.tar.xz
mingwrt-5.4.2-mingw32-man.tar.xz
mpc-1.1.0-1-mingw32-dev.tar.xz
mpc-1.1.0-1-mingw32-info.tar.xz
mpc-1.1.0-1-mingw32-lic.tar.xz
mpfr-4.0.2-1-mingw32-dev.tar.xz
mpfr-4.0.2-1-mingw32-doc.tar.xz
mpfr-4.0.2-1-mingw32-info.tar.xz
mpfr-4.0.2-1-mingw32-lic.tar.xz
ncurses-6.2-2-mingw32-bin.tar.xz
ncurses-6.2-2-mingw32-doc.tar.xz
ncurses-6.2-2-mingw32-lic.tar.xz
ncurses-6.2-2-mingw32-man.tar.xz
pexports-0.48-mingw32-bin.tar.xz
pexports-0.48-mingw32-doc.tar.xz
pexports-0.48-mingw32-lic.tar.xz
terminfo-6.2-2-mingw32-data.tar.xz
w32api-5.4.2-mingw32-dev.tar.xz
wsl-features-20190122-1-mingw32-cfg.tar.xz
xz-5.2.5-2-mingw32-bin.tar.xz
xz-5.2.5-2-mingw32-doc.tar.xz
xz-5.2.5-2-mingw32-ext.tar.xz
xz-5.2.5-2-mingw32-lang.tar.xz
xz-5.2.5-2-mingw32-lic.tar.xz
xz-5.2.5-2-mingw32-man.tar.xz
zlib-1.2.11-1-mingw32-doc.tar.xz
zlib-1.2.11-1-mingw32-lic.tar.xz
zlib-1.2.11-1-mingw32-man.tar.xz

  • 各パッケージの中には、bin/, include/, lib/ などが入っている。MinGWインストールフォルダの bin/, include/, lib/ にそのまま入れるのだろう。
  • *bin* や *dll* が、実行に必要なファイルが入ってるパッケージ。
  • *dev* が、そのライブラリを使ってコンパイル作業をする際に必要になるパッケージ。
  • *doc* や *man* は、おそらくドキュメント。無くてもなんとかなりそう。
  • *lic* は、ライセンス文書。これも無くてもなんとかなりそう。
  • *lang* は、多言語対応用のリソースだろうか。英語のメッセージで十分なら無くてもどうにか。

つまり、上記のパッケージファイルをどうにか入手して、解凍して、SourceForge版のファイル群と差し替えてやれば、OSDN版 MinGW 相当になるのではないのかなと…。考えが甘いだろうか…?

ミラーサーバについて :

OSDN版のパッケージは他のサーバでミラーリングしてないのだろうか。ググってみたら、以下のメモに遭遇。ありがたや。

_OSDNのミラーコンテンツ 2023/11/19

日本国内のサーバに限れば、IIJ と JAIST に残ってる可能性があるらしい。ありがたや。これは助かりそう。

_https://ftp.iij.ad.jp/pub/osdn.jp/storage/g/m/mi/mingw/
_https://ftp.jaist.ac.jp/pub/sourceforge.jp/storage/g/m/mi/mingw/

この中から、前述のファイル群を探し出して入手していけば、どうにかなるのかな…。いや、分からんけど…。

でも、探し出すの、どう考えても面倒臭いよな…。どうせ全部で100MBも無いから、 _上記のファイル群をzipにして、 試しに OneDrive に置いてみます。問題があったら削除しますので…。まあ、上記のミラーサーバのディレクトリ内を探していけば全く同じものが入手できるだろうけど。

MinGW以外の選択肢 :

MinGW が入手できなくなりそうで困ったなと書いてみたけど。今現在、Windows上で gcc/g++ (C/C++コンパイラ) を使いたいなら、MSYS2 を導入するのが妥当な気もする。MSYS2 なら、32bit版のみ対応の MinGW と違って、i686版(32bit版)、x86_64版(64bit) も選べるし。MinGW と比べて、より多くのビルド済みパッケージが用意されていて便利な面もあるし。

_MSYS2


あるいは、そもそも、「Windows上で C++ を使いたい!」なら、Microsoft が公式に用意してくれている Visual Studio Community を利用するのが無難な気もする。個人利用や教育/学術目的なら無料で利用させてもらえるし…。

_Visual Studio Tools のダウンロード - Windows、Mac、Linux 用の無料インストール


自分の場合、gcc/g++ を使って Windows用のスクリーンセーバを作ってみたいと思ってしまったので、スクリーンセーバ用ライブラリ libscrnsave.a が用意されている MinGW にこだわっているけれど。 *1

  • 「スクリーンセーバ? そんなもの作らないよ」と言うことなら MSYS2 で問題無し。むしろ MSYS2 のほうが便利かも。
  • 「Microsoft公式の開発ツールを使えば妙なところでハマらなくて済むだろう」「利用者も圧倒的に多いから参考資料も多い」と言うことなら Visual Studio Community を選ばない理由は無いし。
  • バージョンが古くてもいいなら、まだ SourceForge版 MinGW が入手できる。というか自分も今現在 SourceForge版 MinGW で作業してるし…。

そんなわけで、「OSDNが死んだら Windows上で無料で利用できる C/C++コンパイラが無くなってしまう!」という状態ではないわけで。

とは言え、「昔は新しいパッケージを利用することもできたけど、今は利用できない」という状況もなんだかなあ、と…。いや、単に新しいパッケージを使いたいだけなら MSYS2 を使えばいいんだろうけど…。

*1: MSYS2 に入ってる libscrnsave.a は中身が空なので…。

#2 [basic] FreeBASICとSDL2_mixerでoggを再生できるか試した

Windows10 x64 22H2 + FreeBASIC 1.10.1 32bit でサウンドを鳴らしてみたい。できれば ogg (Ogg Vorbis)を再生したい。

Ogg Vorbis (正確には Vorbis) というのは非可逆圧縮の音声ファイル。mp3 みたいなもの。mp3 と違ってライセンスが緩く、ギャップレス再生もできるので、mp3 よりもゲーム制作に利用し易い。

_Vorbis - Wikipedia

方法についてググってみたら以下の解説記事に遭遇。

_Simplest code to play MP3 or OGG with SDL - freebasic.net
_Windows Ini File FreeBASIC

SDL2 と SDL2_mixer を利用すれば鳴らすことができるようだなと…。

ちなみに、FreeBASIC は、標準状態では beep を鳴らすことしかできないらしい。NEC PC-8001レベルだなあ…。

_FaqPgrtlib

SDL2関連ファイルの入手 :

oggを再生するために、SDL2 と SDL2_mixer を FreeBASIC に導入しないといけない。

当初、SDL2-devel-2.29.2-mingw.zip と SDL2_mixer-devel-2.8.0-mingw.zip を入手して、中に入っていた .dll や .a を利用しようとしたのだけど、それでは正常動作してくれなかった。

FreeBASICインストールフォルダ/inc/SDL2/*.bi を眺めたところ、各ファイルの最初のあたりに、利用を想定している SDL2 のバージョンが書いてあることに気づいた。FreeBASIC 1.10.1 の時点では、それぞれ以下のバージョンが推奨されているらしい。

SDL.bi       : SDL2-2.0.14
SDL_image.bi : SDL2_image-2.0.5
SDL_Mixer.bi : SDL2_mixer-2.0.4
SDL_net.bi   : SDL2_net-2.0.1
SDL_ttf.bi   : SDL2_ttf-2.0.15

SDL2_gfx_framerate.bi       : SDL2_gfx-1.0.4
SDL2_gfx_imageFilter.bi     : SDL2_gfx-1.0.4
SDL2_gfx_primitives.bi      : SDL2_gfx-1.0.4
SDL2_gfx_primitives_font.bi : SDL2_gfx-1.0.4
SDL2_gfx_rotozoom.bi        : SDL2_gfx-1.0.4

これらのバージョンを入手しないと…。以下からSDL2関連ファイルを入手する。

_SDL2 - Index of /release
_SDL2_image - Index of /projects/SDL_image/release
_SDL2_mixer - Index of /projects/SDL_mixer/release
_SDL2_net - Index of /projects/SDL_net/release
_SDL2_ttf - Index of /projects/SDL_ttf/release

入手するファイルは以下。ファイル名に mingw と記載されてるファイルを入手する。今回は SDL2 と SDL2_mixer を利用できればいいので他のファイルは要らんのだけど、ついでなので…。
SDL2-devel-2.0.14-mingw.tar.gz
SDL2_image-devel-2.0.5-mingw.tar.gz
SDL2_mixer-devel-2.0.4-mingw.tar.gz
SDL2_net-devel-2.0.1-mingw.tar.gz
SDL2_ttf-devel-2.0.15-mingw.tar.gz

解凍すると、中には i686-w64-mingw32/, x86_64-w64-mingw32/ が入っている。i686が32bit版、x86_64 が64bit版。
  • bin/*.dll が実行に必要なDLLファイル。
  • lib/*.a が、コンパイル時に必要になるライブラリファイル。

*.dll と *.a、どちらもプロジェクトフォルダにコピーしてしまってもいいし、あるいは、*.a を、FreeBASICインストールフォルダ/lib/win??/ にコピーしてしまってもいい。ただ、SDL2 を使っているプログラムを実行する際は、同じフォルダ内に SDL2関係の .dll が必要になる、と思う。たぶん。

今回必要になるDLLファイルは以下になる。
# SDL2

SDL2.dll
sdl2-config

# SDL2_mixer

libFLAC-8.dll
libmodplug-1.dll
libmpg123-0.dll
libogg-0.dll
libopus-0.dll
libopusfile-0.dll
libvorbis-0.dll
libvorbisfile-3.dll
SDL2_mixer.dll


コンパイル時に必要になりそうな .a ファイルは以下。
# SDL2

libSDL2.a
libSDL2.dll.a
libSDL2_test.a
libSDL2main.a

# SDL2_mixer

libSDL2_mixer.a
libSDL2_mixer.dll.a

サンプルソース :

前述の解説記事からコピペして、サンプルソースを作成。

_ogg_play_sdl2.bas
' ogg play with SDL2 and SLD2_mixer
' request : SDL2 2.0.14, SDL2_mixer 2.0.4

#include "SDL2/SDL_mixer.bi"
dim mymusic as Mix_Music ptr

chdir exepath()

Mix_OpenAudio(44100, MIX_DEFAULT_FORMAT, 2, 4096)

' Here your music file in *.mp3 or *.ogg
mymusic = Mix_LoadMUS("bgm.ogg")

Mix_VolumeMusic(128)  ' 128 is the maximum volume

' 0 play once / -1  play infinit loop
' Mix_PlayMusic(mymusic, 0)

print "Press 1 to stop music."
print "Press 2 to play music."
print "Press 3 to pause music."
print "Press 4 to resume music."
print "Press space key to exit program."

Dim As Boolean running = True
While running
    Dim As String k = inkey$
    Select Case k
    Case "1": Mix_HaltMusic()
    Case "2": Mix_PlayMusic(mymusic, -1)
    Case "3": Mix_PauseMusic()
    Case "4": Mix_ResumeMusic()
    Case " ": running = False
    End Select
    
    sleep 200
Wend

FINALIZATION:
Mix_CloseAudio()

今回は、bgm.ogg というファイルを読み込ませている。

_bgm.ogg

fbc ogg_play_sdl2.bas でコンパイル。ogg_play_sdl2.exe が生成された。

実行してみると以下のメッセージが表示される。
> ogg_play_sdl2.exe
Press 1 to stop music.
Press 2 to play music.
Press 3 to pause music.
Press 4 to resume music.
Press space key to exit program.

1, 2, 3, 4, Spaceキーで、oggを停止、再生、ポーズ、リジューム(ポーズ解除)、プログラムの終了ができた。

ということで、FreeBASIC + SDL2 + SDL2_mixer を使えば、ogg (Ogg Vorbis) が再生できると分かった。ゲームのBGMとして鳴らせそうではあるなと…。しかし、BGMはこれでなんとかなるとして、SE はどうすれば…? 複数のチャンネルで鳴らせないと話にならないよな…。

余談。SDL1.xについて :

FreeBASIC は、SDL (SDL 1.x) と SDL2 (2.x) を使えるようで、今回は SDL2 を使ったわけだけど。

先日、SDL (SDL 1.x) を利用できるか実験したときは、MSYS2 の mingw32/ 以下から *.a を持ってきて試してみたら動いてしまったわけで…。アレって、SDL 1.x のバージョンは合ってたのだろうか? 不安になってきたので調べてみた。

FreeBASICインストールフォルダ/inc/SDL/*.bi に記述されてる、SDL 1.x の推奨バージョンは以下になっていた。
SDL.bi       : SDL-1.2.15
SDL_image.bi : SDL_image-1.2.12
SDL_mixer.bi : SDL_mixer-1.2.12
SDL_net.bi   : SDL_net-1.2.8
SDL_ttf.bi   : SDL_ttf-2.0.11

SDL_gfx_framerate.bi       : SDL_gfx-2.0.26
SDL_gfx_imageFilter.bi     : SDL_gfx-2.0.26
SDL_gfx_primitives.bi      : SDL_gfx-2.0.26
SDL_gfx_primitives_font.bi : SDL_gfx-2.0.26
SDL_gfx_rotozoom.bi        : SDL_gfx-2.0.26

MSYS2側のパッケージのバージョンは以下になっていた。(x:/msys64/var/cache/pacman/pkg/mingw-w64-i686-SDL*)
mingw-w64-i686-SDL-1.2.15+r419+gef3a6c05-1-any.pkg.tar.zst
mingw-w64-i686-SDL_image-1.2.12-8-any.pkg.tar.zst
mingw-w64-i686-SDL_mixer-1.2.12-9-any.pkg.tar.zst
mingw-w64-i686-SDL_net-1.2.8-3-any.pkg.tar.zst
mingw-w64-i686-SDL_ttf-2.0.11-6-any.pkg.tar.zst
mingw-w64-i686-SDL_gfx-2.0.27-1-any.pkg.tar.zst

SDL_gfx 以外は、たまたまバージョンが一致していた模様。 *1 だからすんなり動いてしまったのかもしれないな…。

*1: SDL 1.x の最終バージョンだろうから、一致するのは当たり前だろうか…。

2024/01/26(金) [n年前の日記]

#1 [basic] FreeBASICでMIDIを鳴らせるsfxライブラリを試用

FreeBASICで、MIDI、もしくはwavを鳴らせる sfxライブラリと言うものがあると知った。

_Freebasic sfx library download | SourceForge.net
_Freebasic sfx library - Browse Files at SourceForge.net
_ExtLibsfx
_sfx - FreeBASIC Wiki Manual | FBWiki
_Audio library for FreeBasic - Features - freebasic.net

もし、wavを複数同時に鳴らせるなら、ゲーム制作時のSE再生に使えるのではないだろうかと思えてきたので試用してみた。環境は Windows10 x64 22H2 + FreeBASIC 1.10.1 32bit。

結論を先に書くけれど、これはゲーム制作に使えないライブラリだった。と言うのも、複数のwavを同時に鳴らせなくて…。再生指示を出した順に、wavを順番に一つずつ再生していく仕様だった。

まあ、 _MML (Music Macro Language) を鳴らしたり、MIDIを鳴らしたり、MMLをMIDIファイルとして保存できたり、Sin波等の波形をその場で作って再生できるあたりは面白そうだけど…。また、FreeBASICで、FreeBASICのライブラリ(*.a)を生成している点も興味深い。

インストールの仕方 :

一応、インストールの仕方をメモしておく。

  1. sfx.zip を入手して解凍。
  2. 中に sfx/ と言うフォルダがあるので、FreeBASICインストールフォルダ/ の直下にコピー。
  3. sfx/ に入って、buildwindows.bat を実行すると、libfbsfx.a が生成される。-mt がどうのこうのと警告が出ている点は気になるけど…。
  4. libfbsfx.a を、FreeBASICインストールフォルダ/lib/win??/ にコピー。
  5. *.bi を、FreeBASICインストールフォルダ/inc/ にコピー。

inc/ フォルダにコピーした *.bi は以下。
instruments.bi
midisynth.bi
sequencer.bi
sfx.bi
wave.bi

これで、ソースの先頭に以下を書いておけば、sfx が使えるようになる。
#include "sfx.bi"
#inclib "fbsfx"

動作確認 :

FreeBASIC Wiki のサンプルを参考にして動作確認。

_sfx - FreeBASIC Wiki Manual | FBWiki

サンプルを眺めれば分かるけれど、MMLが使える。考えてみたら、8bit PC時代のBASICは、えてしてMMLが使えた記憶もあるな…。「BASICならMMLが使えて当然だろう!」てなノリで当時のソレを再現しようとしているライブラリ、なのだろうか?

MMLの最初の再生時は、数秒ほど待たされる模様。何か重めの初期化処理をしているのかもしれない。

2024/01/27() [n年前の日記]

#1 [basic] FreeBASICでwavやoggを鳴らせるFBSoundライブラリを試用

FreeBASICで、wav、mp3、ogg等を鳴らせる FBSound というライブラリがあると知った。

_jayrm/fbsound: A free sound library primarily for games and demos written by D.J. Peters.
_fbsound 1.2 Windows/Linux (sid wav mp3 ogg mod it xm s3m) - freebasic.net
_fbsound 1.1 (dynamic) Windows/Linux 32 and 64-bit (wav mp3 ogg mod it xm s3m) - freebasic.net
_fbsound 1.0 Win/Lin 32/64-bit (wav mp3 ogg mod it xm s3m) - freebasic.net
_Load Sound in Memory (FBSound 1.2) - freebasic.net

以前はライセンスが不明で敬遠されてた気配があるけど、今現在はドキュメント上で、ライセンスについてちゃんと書いてあるように見える。

Windows10 x64 22H2 + FreeBASIC 1.10.1 32bit で試用してみることにした。複数のwavを同時に鳴らせたらいいのだけど、さて。

インストール :

fbsound-1.2.zip を入手して解凍。
  • inc/ にヘッダーファイル .bi が入っている。
  • test/ に動作に必要な .dll ファイル群が入っている。

inc/ の中身は以下。
fbs3d.bi
fbsound_dynamic.bi
fbsound_oop.bi

test/*.dll は以下。
fbsound-32.dll
fbsound-64.dll
fbsound-ds-32.dll
fbsound-ds-64.dll
fbsound-mm-32.dll
fbsound-mm-64.dll

この .bi と .dll を、利用したいプロジェクトフォルダにコピーして、ソースの最初のほうに以下を書いておけば、FBSound が使えるようになる。
#include "fbsound_dynamic.bi"

あるいは、.bi ファイルを、FreeBASICインストールフォルダ/inc/ にコピーしておいてもいい。ただ、生成した .exe と同じ場所に .dll群も置いておかないと動作しない。

動作確認 :

test/ 以下にサンプルファイル群が入っているので、各 .bas について、fbc xxxx.bas と打ってコンパイルすれば動作確認できる。

ogg (or wav) を鳴らすサンプルを書いてみた。

_waveplay.bas
' wave play sample with FBSound

#include "fbsound_dynamic.bi"

chdir(exepath())

' FBSound initialize
If fbs_Init() = True Then
    Print "fbs_Init() success."
Else
    Print "Error : fbs_Init() failure"
    Print FBS_Get_PlugError()
    Beep
    sleep
    end 1
End If

dim as integer hWave1, hWave2, hWave3

#ifdef 0

' load wav file
Print "load wav file"
fbs_Load_WAVFile("1.wav", @hWave1)
fbs_Load_WAVFile("2.wav", @hWave2)
fbs_Load_WAVFile("3.wav", @hWave3)

#Else

' load ogg file
Print "load ogg file"
fbs_Load_OGGFile("1.ogg", @hWave1)
fbs_Load_OGGFile("2.ogg", @hWave2)
fbs_Load_OGGFile("3.ogg", @hWave3)

#endif

Print "Play : 1 - 3 key"
Print "Exit : ESC key"

Dim As Boolean running = True
Dim As String k

While running
    Dim As String k = inkey$
    Select Case k
    Case "1": fbs_Play_Wave(hWave1)
    Case "2": fbs_Play_Wave(hWave2)
    Case "3": fbs_Play_Wave(hWave3)
    Case Chr(27): running = False
    End Select
    
    sleep 100
Wend

Print "Exit."

fbc waveplay.bas と打ってコンパイル。waveplay.exe が生成される。

_1.ogg_2.ogg_3.ogg を同じフォルダに入れてから実行すれば、1, 2, 3キーを押すことで ogg を再生できる。

複数のoggを同時に再生することができているので、鳴らすだけでOK系のゲームSEなら使えそうかなと…。

modも鳴らせる :

.wav 以外に、.ogg、.mp3 も鳴らせるけれど、.mod, .xm, .s3m, .it 等、Amiga発祥のデモシーンで使われていた音楽ファイルフォーマットも利用できる。

_MOD (ファイルフォーマット) - Wikipedia

#2 [basic] FreeBASIC + FBWindowsSoundWav.basを試用

FreeBASIC でwav再生するためのライブラリについて調べてる。

以下の投稿ページの中で、Windows と Linux の両方でwav再生するための関数名を同じにしたライブラリが紹介されてた。

_Play Sound WAV (Windows [32-bit], [64-bit], Linux [32-bit], [64-bit]) - freebasic.net

再生の実処理は、Windows、もしくは Linuxに特化した機能を呼び出して実現してるらしい。関数名が同じになるだけでも便利そうだなと思えてきたので試用してみることにした。環境は Windows10 x64 22H2 + FreeBASIC 1.10.1 32bit。

結論を先に書いておく。一応wavは鳴ったけど、ゲーム制作用には使えない印象。

インストール :

前述の掲示板から、ソースコードをコピペして、ローカルで .bi や .bas として保存する。
  • Linux の場合は、alsa.bi と FBLinuxSoundWav.bas をプロジェクトフォルダにコピー。
  • Windwos の場合は、FBWindowsSoundWav.bas をプロジェクトフォルダにコピー。

サンプルは以下。今回は 1.wav というファイルを読み込んで再生する。

_sample.bas
#include "FBWindowsSoundWav.bas"

Dim fb_Wav As FbWAV Ptr

LoadWav("1.wav", fb_Wav)
PlayWav(fb_Wav)

Do
    Var key = Inkey
    Select Case key
    Case "s" : StopWav(fb_Wav)
    Case "p" : PlayWav(fb_Wav)
    Case "q" : Exit Do
    Case " "
        PauseWAV(fb_Wav)
        Sleep(100)
    End Select

    Var sp = StatusPlay(fb_Wav)
    Sleep(100)

    Cls
    Select Case sp
    Case 1 : Print "Play"
    Case 0 : Print "Stop"
    Case 2 : Print "Pause"
    End Select

    Print "Length=" & GetLengthSeconds(fb_Wav) & " seconds"
    Print "s)top / p)lay / q)uit / SPACE : Pause"
Loop

FreeWav(fb_Wav)

fbc sample.bas でコンパイル。sample.exe を実行。pキーで再生。sキーで停止。qキーで終了。

たしかに wav が鳴ってくれたけれど…。前の再生がまだ終わってない状態で連続して鳴らすと、前の再生をブツ切りして、次の音が最初から再生されてしまう。同時に並行して鳴ってほしいのだけどなあ…。

また、ブツ切りされる際に、ノイズもブチブチと入りまくる。

ゲーム制作に使えるライブラリでは無さそうだなと…。

#3 [basic] FreeBASIC + FMODを試用

FreeBASIC から、FMOD (fmod.dll) なるサウンド用ライブラリを利用することができるらしい。

_FMOD
_FMOD - FreeBASIC Wiki Manual | FBWiki
_FreeBASIC Compiler / Feature Requests / #185 FMOD Ex headers for freebasic...
_TUTORIAL: Using FMOD for In-Game Music - Page 2 - freebasic.net

FMODは、商用利用時はライセンス料を払わないといけないサウンドライブラリ。ただ、フリーソフトに使う分には無料で利用しても良いことになっているらしい。あるいは、売り上げが一定額に満たない場合も無料で利用できる、と書いてあるように見えた。昔の Unity みたいな感じだろうか。 *1

気になったので、Windows10 x64 22H2 + FreeBASIC 1.10.1 32bit で試用してみた。

インストール :

現行版のバイナリはユーザ登録しないと入手できないように見えたのだけど、おそらく昔の版なら、別の経路で入手できそう。例えば、以下のページから、UsingFMOD_A_Tutorial.zip もしくは、fmod_example_program.zip を入手する。中に fmod.dll も入ってる。

_TUTORIAL: Using FMOD for In-Game Music - Page 2 - freebasic.net
_QB Express: Issue #26

fmod_example_program.zip を解凍すると、サンプルファイル fmod_example.bas と fmod.dll が入ってる。ちなみに、ヘッダーファイル、fmod.bi は、FreeBASIC 1.10.1 に最初から入っている。

動作確認 :

fbc fmod_example.bas でコンパイル。

ただ、警告が結構出る。もしかすると fmod.dll のバージョンが合ってないのだろうか。しかし、FreeBASIC の fmod.bi には FMOD 3.75 と記述されていて、今回入手できた fmod.dll のバージョンも 3.7.5。バージョンは合ってそうな気がする…。FreeBASIC のバージョンアップに伴う仕様変更に、fmod.bi が追従していないだけだろうか? 随分古いライブラリのようだし。

さておき。一応、wav は鳴ってくれた。別々の音が同時に鳴っているようにも聞こえる。

しかし、一部の wav がループ再生されてしまう…。全ての wav がループ再生されるわけではなくて、特定のwavだけがループ再生になる。何故。ワンショット系のSEにするにはどうしたらいいのか…。

もしかして wav ファイルの中にループ再生するかしないかの情報を盛り込めることができて、たまたま何かの拍子に、その wav にだけ、その情報が入っちゃってるとか…?

ループ再生されてしまう wav を SoundEngine Free 5.23 で開いて、そのまま保存し直してみた。保存し直した wav を使ったらループ再生されない。どういうことだろう…。分からんなあ…。やはり wav にループ再生を指示する何かしらの情報を埋め込めるのだろうか…。

*1: 今の Unity はどういう価格体系になったのかよく知らないので、「昔の」と言っておきます…。

#4 [basic] FreeBASIC + BASSを試用

FreeBASIC から BASSなるサウンド用ライブラリを利用することができるらしい。

_Un4seen Developments - BASS
_Un4seen Developments - BASSMOD

BASSは、非営利目的の場合は無料で利用できる。商用利用の場合はライセンス料を払わないといけない。

たしかこのあたり、DXRuby を触ってた頃も、「作ったゲームを同人ソフトとして販売するだけでも一定額を払わないといけないのはどうなのか。そのゲームが売れるのか売れないのか、全然分からないのに」みたいな話があって敬遠されていた記憶が…。

でもまあ、フリーソフトを作る分には無料で利用できるので、一応試用。環境は Windows10 x64 22H2 + FreeBASIC 1.10.1 32bit。

インストール :

以下のファイルを入手。
bass24.zip
bassmod20.zip

後者は、modファイルを再生するために必要。wav や mp3 を鳴らすだけなら要らない、と思う。たぶん。

それぞれ解凍して、中に入っている .dll (bass.dll, BASSMOD.dll) をプロジェクトフォルダにコピーしておく。

ちなみに…。
  • bass24.zip : 32bit版と64bit版の dll が入っている。
  • bassmod20.zip : 32bit版の dll しか入ってない。

動作確認 :

FreeBASICインストールフォルダ/examples/sound/BASS/ の中にサンプルファイルがある。

demo.bas と small.bas はコンパイルできて、ogg と mo3(oggに対応させたmodらしい)は再生できた。

しかし、mod.bas がコンパイルできない。以下のエラーを出す。
> fbc mod.bas
mod.bas(15) error 58: Type mismatch, at parameter 2 of BASSMOD_MUSICLOAD() in 'If (BASSMOD_MusicLoad(FALSE, sound_file, 0, 0, BASS_MUSIC_LOOP) = False) Then'

色々調べてみたけれど、FreeBASICインストールフォルダ/inc/bassmod.bi の、以下の行を変更したら、コンパイルできるようになった。file のタイプを const zstring ptr に変更している。
declare function BASSMOD_MusicLoad(byval mem as BOOL, byval file as any ptr, byval offset as DWORD, byval length as DWORD, byval flags as DWORD) as BOOL

' ↓

declare function BASSMOD_MusicLoad(byval mem as BOOL, byval file as const zstring ptr, byval offset as DWORD, byval length as DWORD, byval flags as DWORD) as BOOL

しかし、コンパイルはできたものの、生成された mod.exe が、Windowsセキュリティから PUA:Win32/Keygen として警告されてしまう。mod.exe を、オンラインのウイルススキャンサイトに渡してスキャンしてみると問題無しと出てくるので、単に誤検出じゃないのかと思うけど…。

どうせ誤検出だろう、誤検出に違いないと信じて(?)、mod.exe を実行してみたところ、一応 mod が鳴ったことは鳴ったけれど…。ウイルス扱いされるのでは困るよなあ…。生成物を配布できないやん…。

いやまあ、BASSMOD は使わず、BASSだけ使う分には、そういう問題に遭遇しないのかもしれんけど。今の時代に、あえてゲームのBGMとしてmod/s3m/xm/itを鳴らしたい人も居ないだろうから、それほど実害は無さそうな気も…。BGMの類は、えてして ogg か mp3 にして用意することが多いだろうし…。

#5 [basic] FreeBASIC + PortAudioを試用してみたかった

FreeBASICから PortAudio なるサウンドライブラリを利用することができるという話も見かけたのでそのあたりを試してみた。環境は Windows10 x64 22H2 + FreBASIC 1.10.1 32bit。

結論を先に書くけど、PortAudio についての知識を持ってる人じゃないと使えない気配がした。

PortAudioをMinGWでビルド :

PortAudio を使うためには、ライブラリファイル *.a や、ランタイム(?) *.dll が必要になるのだろう。MinGW(gcc 6.3.0) を使ってビルドできないか試した。

_PortAudio - an Open-Source Cross-Platform Audio API
_Notes_about_building_PortAudio_with_MinGW ・ PortAudio/portaudio Wiki
_PortAudio: Building Portaudio for Windows with MinGW

FreeBASICに同梱されている portaudio.bi を眺めたところ、v19_20140130 を対象にしていた。同じバージョンを入手してビルドしたほうが良さそうだな…。

PortAudio のサイトから、pa_stable_v19_20140130.tgz を入手して解凍。

MinGW/MSYS が使える状態にして、portaudio/ の中に入って、bash を起動。configure を利用する関係で、bash上でしか作業できない。

以下を打ったところ、色々なファイルが生成された。
./configure
make

include/ にヘッダファイル、*.h がある。これはビルドする前から元々入っている。

lib/ 以下に、色々なファイルが生成された。更に、lib/.libs/ に以下のファイルが入っている。
libportaudio.dll.a
libportaudio.la
libportaudio-2.dll
  • *.a や *.la が、コンパイル時に必要になるライブラリファイル。
  • *.dll が、実行時に必要になるDLLファイル。

FreeBASICで使用 :

FreeBASIC 1.10.1 32bitで使えそうか試してみる。libportaudio.dll.a, libportaudio.la を、freeBASICインストールフォルダ/lib/win??/ にコピー。

加えて、libportaudio-2.dll をプロジェクトフォルダにコピーするのだけど、これだけでは動かなかった。libmingwex-0.dll とやらも必要になった。MinGWインストールフォルダ/bin/ から探して持ってくる。

ただ、サンプルファイルが皆無なので、使い方が全く分からない…。

別版バイナリを試用 :

FreeBASICの掲示板で、ビルド済みの別版 PortAudio バイナリを配布している事例を見かけた。

以下の投稿ページで、FreeBASIC用にビルドした PortAudio のバイナリが配布されている。FreeBASICの配布パッケージに入ってるものとは異なる、別の版らしい。

_libportaudio-2 dynamic windows linux 32/64-bit - freebasic.net

portaudio.zip を入手して解凍すると、以下のファイルが入っていた。
.
|-- lib
|   |-- lin32                   (Linux 32bit)
|   |   `-- libportaudio-2.so
|   |-- lin64                   (Linux 64bit)
|   |   `-- libportaudio-2.so
|   |-- win32                   (Windows 32bit)
|   |   `-- libportaudio-2.dll
|   `-- win64                   (Windows 64bit)
|       `-- libportaudio-2.dll
|-- enumerate.bas               (sample)
|-- pa_dynamic.bi               (FreeBASIC Header file)
`-- streamcallback.bas          (sample)

5 directories, 7 files

見た感じ、以下のようなファイルなのだろう。
  • Windows 32bit, 64bit用の libportaudio-2.dll
  • Linux 32bit, 64bit用の libportaudio-2.so
  • FreeBASIC用のヘッダーファイル pa_dynamic.bi
  • サンプルファイル enumerate.bas, streamcallback.bas

Wndows 32bit用の libportaudio-2.dll をサンプルファイルと同じ場所にコピーして、fbc streamcallback.bas でコンパイル。streamcallback.exe を実行したらサイン波が鳴ってくれた。

fbc enumerate.bas で enumerate.exe を生成して実行したところ、利用できるデバイスの一覧が表示された。
PortAudio V19-devel (built May 13 2016 04:39:45)

(0) input  Microsoft Sound Mapper - Input
(1) input  マイク (Realtek(R) Audio)
(2) input  ステレオ ミキサー (Realtek(R) A
(3) output Microsoft Sound Mapper - Output
(4) output スピーカー (Realtek(R) Audio)
(5) output Realtek Digital Output (Realtek

これってつまり、PortAudio を使って音を鳴らしたい時は、どれかしらのデバイスを明示的に指定しないと鳴らせないということになるのだろうか…? なんだか面倒臭いな…。

更に別の版を試用 :

上記の版とはまた別の版も見かけた。以下のやり取りの下のほうで、fbPortAudio.zip というファイルが配布されている。

_C/C++ Libraries - freebasic.net

fbPortAudio.zip を入手して解凍したら、*.a, *.dll, *.bi が入っていた。

以下のサンプルファイルをコンパイルしたら、情報を表示したり、サイン波が鳴った。
patest_devs.bas
patest_sine.bas
patest_version.bas

状況が混乱している気がする :

FreeBASIC + PortAudio は、状況的によく分からないことになっている気がする…。

公式の portaudio.bi とは別の版が複数あるようで、一応それらは、付属している .dll を一緒に利用する限り、それらしく動作する。

しかし、公式の portaudio.bi は、使い方が分からない。他の版のサンプルを流用してコンパイルしようとすると、各関数に与える変数のタイプが異なっているようで、エラーが出てコンパイルできない。

加えて、サンプルソースを見ても、実際に音を出すまでに細々とした各種設定をしないといけないようで、何をしているのかさっぱり分からない。

とにかく、公式版に同梱されている portaudio.bi を利用したサンプルが一切存在していない点が非常に困る…。自分でビルドした *.a や *.dll が、ちゃんとビルドできているのか、簡単な動作確認すらできない…。

この、PortAudio、自分の知識レベルではちょっと手に負えないライブラリのようだなと…。

2024/01/28() [n年前の日記]

#1 [basic] FreeBASIC + raylibを試用

raylibという、ゲーム制作用のフレームワークがあるらしい。

_raysan5/raylib: A simple and easy-to-use library to enjoy videogames programming
_raylib | A simple and easy-to-use library to enjoy videogames programming

元々は C言語用のライブラリだったけど、色々な言語から呼び出して使えるようになってる模様。

_raylib/BINDINGS.md at master - raysan5/raylib

FreeBASICから利用することもできる、という話を見かけた。本当に動かせるのか気になったので、少しだけ試してみた。

環境は Windows10 x64 22H2 + FreeBASIC 1.10.1 32bit/64bit。

色んな版があった :

FreeBASIC から raylib を利用するには、ヘッダーファイルが必要。

FreeBASIC 1.10.1 のインストールフォルダ/inc/ に raylib.bi というヘッダーファイルが既に入っていた/同梱されていたけれど、中身を眺めてみたら raylib 3.0.0 用だった。raylib の現行版は 5.0.0 なので、FreeBASICのソレは古い気がする…。

githubで検索したところ、4種類のFreeBASIC用ライブラリがあった。

_WIITD/raylib-freebasic: FreeBasic bindings for raylib
_glasyalabolas/fb-raylib: Port of raylib 3.5 headers and examples for FreeBasic
_denise-amiga/raylib-fb: FreeBasic bindings for raylib
_VB6Hobbyst7/FB_3D_world_DEMO: a 3D world coding exercise in freeBASIC using raylib

上から順に、raylib 5.0/4.5/4.2対応、3.5対応、3.0対応、3.0対応、となっていた。

更に、FreeBASICの掲示板で配布してる版も見かけた。raylib 3.0用らしい。

_fbraylib 3.0 dynamic/static Windows/linux 32/64 bit. - freebasic.net

raylib 5.0対応版 raylib-freebasic を試用 :

新しいほうがいいのかなと、5.0対応版の raylib-freebasic を試用してみることにした。

_WIITD/raylib-freebasic: FreeBasic bindings for raylib

raylib-freebasic-5.0.zip を入手して解凍。

raylib本体も入手しないといけない。どこかで、raylib はビルドにMINGW64が必要と書いてあったような気がするので、raylib 本体は、64bit版の raylib-5.0_win64_mingw-w64.zip を選ぶことにした。

_raysan5/raylib: A simple and easy-to-use library to enjoy videogames programming

raylib-5.0_win64_mingw-w64.zip を入手して解凍。

FreeBASIC用の raylib-freebasic-5.0.zip を解凍してできたフォルダの中には、.bi とサンプル(examples/)が入っていた。このフォルダの中に、raylib 5.0 本体側の .a と .dll をコピーしてやる。

  • libraylib.a, libraylibdll.a を、raylib-freebasic の .bi と同じ階層にコピー。
  • raylib.dll は、raylib-freebasic のサンプルファイルがある階層にコピー。

FreeBASIC 64bit版の fbc64.exe を使ってサンプルファイルをコンパイル。
fbc64 core_basic_window.bas
core_basic_window.exe が生成された。

実行するとウインドウが開いた。ウインドウ右上の閉じるボタンをクリックしたら、ちゃんと閉じてくれた。

しかし、2つあるサンプルファイルのうち、1つがコンパイルできない。
> fbc64 core_3d_camera_first_person.bas
core_3d_camera_first_person.bas(27) error 42: Variable not declared, SetCameraMode in 'SetCameraMode(cam, CAMERA_FIRST_PERSON)'
core_3d_camera_first_person.bas(33) error 1: Argument count mismatch in 'UpdateCamera(@cam)'

同梱サンプルすらコンパイルできない状態で配布されているあたり、もしかすると鋭意開発中状態なのだろうか。

raylib 3.5対応版 fb-raylib を試用 :

サンプルファイルの数は 3.5対応版のほうが圧倒的に多いので、3.5対応版の fb-raylib を試用してみることにした。

_glasyalabolas/fb-raylib: Port of raylib 3.5 headers and examples for FreeBasic

fb-raylib-master.zip を入手して解凍。

加えて、raylib 3.5本体を入手するのだけど…。
  • 何故か raylib 32bit版を使わないと、一部のサンプルファイルにおいて、freeBASIC側でのコンパイルに失敗した。
  • 試しに raylib 3.7 を入手して使ってみたが、これも一部のサンプルファイルがコンパイルに失敗した。
あくまで raylib 3.5 32bit版を使わないといかんらしい…。

raylib-3.5.0_win32_mingw-w64.zip を入手して解凍。lib/ の中に、.a と .dll があるので…。
  • .a は、.bi がある場所にコピー。
  • .dll は、サンプルファイル .bas がある場所(examples/ の中)にコピー。

32bit版 FreeBASICを使ってコンパイル。
fbc32 core_basic_window.bas
core_basic_window.exe が生成されたので実行。ウインドウが表示された。ウインドウ右上の閉じるボタンをクリックしたら、ちゃんとウインドウが閉じた。

他のサンプルファイルもコンパイルして動作確認してみたけれど、どれもコンパイルが通って動いてくれた。

今のところ、この版、raylib 3.5.0 win32 mingw w64 と fb-raylib の組み合わせが、一番すんなりと動いてくれる気がする。

FreeBASIC同梱版を試用 :

FreeBASICに同梱されていた raylib.bi も使ってみた。これは raylib 3.0.0 の利用を推奨してる。

raylib 3.0.0 本体、raylib-3.0.0-Win32-mingw.zip を入手して解凍。.a と .dll を、サンプルファイルと同じ場所に置いて、fbc32 でコンパイル。

生成した exe を実行しようとすると raylib.dll が無いと言われる。libraylib.dll ならあるのだけど…。raylib.dll にリネームしたら動いてくれた。

しかし、ウインドウは表示されるものの、閉じるボタンを押しても反応しない。DOS窓上から実行していたので、DOS窓上で Ctrl+C を押したら止まってくれた。どうやら、raylib.bi 内の、WindowShouldClose() の返り値が Byte になってるのがダメらしい。Boolean に修正したらウインドウを閉じられるようになった。しかし、ウインドウを閉じることすらできない状態で同梱されているとは…。

FreeBASIC同梱版が一番問題無く使えるのかなと予想したけれど、そういうわけでもなかった…。

そもそも同梱版は、対象にしている raylib のバージョンが古い点も気になる。おそらく、とりあえず動くようになった時点で同梱されて、そこからずっと放置されているのでは…。もしかすると FreeBASIC + raylib で何かを作りたいという人はそれほど居なくて、動作確認も、バグ報告もされていないのかもしれない。

何にせよ試した範囲では、今現在は raylib 3.5対応版の fb-raylib で実験したほうがトラブルに遭遇することも少ないのではないかなあ、と…。

余談。dllが入ってない版もある :

raylib 4.2, 4.5, 5.0 32bit版の中には .dll が入ってなかったりするのだけど…。32bit版はサポートしなくなったということだろうか…。それとも Visual C++版の dll を流用しろということ…?

特に 4.2 は lib/ の中身まで空になっていた。どうも事情があるらしいけれど詳しいことはよく分からない。

_[build] raylib-4.2.0_win32_mingw-w64.zip missing libs - Issue #2698 - raysan5/raylib

2024/01/29(月) [n年前の日記]

#1 [basic] FreeBASICでSDL 1.xを使ってみたかった

_昨日、 FreeBASIC + ゲーム制作用ライブラリ raylib を試用してみたけれど。ゲーム制作用ライブラリなら SDL があるのではないか、FreeBASIC から SDL も使えるらしいのでそちらを使うのもアリではないのかと思えてきた。

でも、本当に FreeBASIC からSDLを使えるのだろうか。気になったので、少し試してみた。

環境は Windows10 x64 22H2 + FreeBASIC 1.10.1 32bit。

サンプルファイルの在処 :

FreeBASICには、SDL 1.x/2.xを使うサンプルファイルが同梱されてる。examples/graphics/SDL/*.bas がサンプルファイル群。

_fbc/examples/graphics/SDL at master - freebasic/fbc

コレが動けば、FreeBASIC + SDL が使える状態になっていると言えそう。

ちなみに、サンプル中の、sdl2-hello.bas だけが SDL2 を使うサンプルで、それ以外は SDL 1.x を使っているサンプルになっている。

要求されるバージョン :

FreeBASICインストールフォルダ/inc/SDL/ に、SDL 1.x用のヘッダーファイルがある。

_fbc/inc/SDL at master - freebasic/fbc

各ヘッダーファイル内に、当時対象にしていたのであろう、SDLのバージョンが書いてあったのでメモしておく。バージョンを合わせておいたほうがハマらずに済みそう。

SDL.bi       : SDL-1.2.15
SDL_image.bi : SDL_image-1.2.12
SDL_mixer.bi : SDL_mixer-1.2.12
SDL_ttf.bi   : SDL_ttf-2.0.11
SDL_net.bi   : SDL_net-1.2.8

SDL_gfx_framerate.bi       : SDL_gfx-2.0.26
SDL_gfx_imageFilter.bi     : SDL_gfx-2.0.26 
SDL_gfx_primitives.bi      : SDL_gfx-2.0.26
SDL_gfx_primitives_font.bi : SDL_gfx-2.0.26
SDL_gfx_rotozoom.bi        : SDL_gfx-2.0.26


ついでなので、SDL2用ヘッダーファイルに書かれていた SDL2のバージョンもメモしておく。

_fbc/inc/SDL2 at master - freebasic/fbc

SDL.bi       : SDL2-2.0.14
SDL_image.bi : SDL2_image-2.0.5
SDL_Mixer.bi : SDL2_mixer-2.0.4
SDL_net.bi   : SDL2_net-2.0.1
SDL_ttf.bi   : SDL2_ttf-2.0.15

SDL2_gfx_framerate.bi       : SDL2_gfx-1.0.4
SDL2_gfx_imageFilter.bi     : SDL2_gfx-1.0.4
SDL2_gfx_primitives.bi      : SDL2_gfx-1.0.4
SDL2_gfx_primitives_font.bi : SDL2_gfx-1.0.4
SDL2_gfx_rotozoom.bi        : SDL2_gfx-1.0.4

必要になるファイル :

FreeBASICから SDL 1.x/2.x を使うためには、MinGW用ライブラリファイル libsdl*.a と、SDLのランタイムファイル SDL*.dll が必要になる。

ランタイムファイル SDL*.dll については、公式サイトで配布されているので容易に入手できる。

_SDL - Index of /release
_SDL_image - Index of /projects/SDL_image/release
_SDL_mixer - Index of /projects/SDL_mixer/release
_SDL_ttf - Index of /projects/SDL_ttf/release
_SDL_net - Index of /projects/SDL_net/release

例えば SDL 1.2.15 の場合、以下の2つのファイルに .dll が入っている。x64とついているのが 64bit版。ついてないのが 32bit版。
SDL-1.2.15-win32.zip
SDL-1.2.15-win32-x64.zip
SDL_image や SDL_mixer 等も、上記と似たようなファイル名になっている。


問題は、MinGW用ライブラリ、libsdl*.a。このファイルの入手が難しい…。

配布ファイル名が「SDL*-devel-*-mingw*.tar.gz」になってたら、そのファイルの中にMinGW用ライブラリが入ってる可能性が高い。例えば以下のファイルには、MinGW用ライブラリが入ってる。
SDL-devel-1.2.15-mingw32.tar.gz
SDL2-devel-2.0.14-mingw.tar.gz
SDL2_image-devel-2.0.5-mingw.tar.gz

しかし…。
  • SDL2.x用ならMinGW用ライブラリも配布されているけれど、SDL 1.x 用は配布されてない。
  • SDL 1.x 本体だけは上記のように配布されているけれど、それ以外、SDL_image や SDL_Mixer等は用意されてない。
SDL 1.x 関係は、ソースファイル、ランタイム、Visual C++用ライブラリファイルしか入手できない。

知識のある人なら、MinGW や MSYS2 を使って、ソースファイルからビルドしてライブラリファイルをゲットできるのだろうけど…。

結論を先に書く。Visual C++用ライブラリ(*.lib)を、MinGW用ライブラリ(*.a)に変換できる reimp.exe というツールがMinGWに用意されてるので、ソレを使って MinGW用ライブラリを用意するのが一番楽。かもしれない。たぶん。

とりあえず、reimp を使って .lib から .a に変換した、SDL関連 MinGW用ライブラリ一式(32bit版のみ)を、zipでまとめて置いておきます。

_sdl_1x_win32_x86_lib_to_a.zip

これを解凍して、*.a と *.dll を、FreeBASIC + SDL 1.x を使うプロジェクトフォルダにごっそりコピーすればどうにかなるかなと…。

動作確認 :

こうして用意できた .a と .dll 群を、examples/graphics/SDL/ の中にコピーして、fbc32 xxxx.bas でコンパイルしていった。

_fbc/examples/graphics/SDL at master - freebasic/fbc

mouse.bas 以外はコンパイルが通って、動作してくれた。

mouse.bas だけコンパイルできない点は少し気になるけど…。ほとんどは動いてくれたから、まあいいか…。

reimpについて :

以降は、自分で lib*.a を用意したい場合の作業メモ。

Visual C++用ライブラリは拡張子が .lib。MinGW用ライブラリは拡張子が .a。そのままでは Visual C++ のライブラリを MinGW で使えない。 *1

しかし、MinGW の mingw-utils の中に、reimp.exe というツールがあって、コレを使うと .lib を .a に変換できる(可能性がある)と知った。

_MinGW - Minimalist GNU for Windows - Browse /MinGW/Extension/mingw-utils at SourceForge.net
_ohai日誌(2007-03-05)
_ソフトに関する雑記

ただ、注意点がある。mingw-utils は 0.4-1 以外を使うこと。

今回、手元の環境に mingw-utils-0.4-1 が入っていたので、reimp が使えるか試してみたのだけど、変換できないと言われてしまった。
> reimp SDL_mixer.lib
reimp: SDL_mixer.lib: invalid or corrupt import library

どの .lib も全く変換できないのでおかしいなと思ったら、mingw-utils-0.4-1 に入ってる版はバグがあるそうで…。

_MinGW GCC toolchain (Win) - TrinityCore - Confluence
If you are getting errors with the reimp command, you may need to rebuild the mingw-utils-0.4-1 package from source as the binary distribution contains a bug.

MinGW GCC toolchain (Win) - TrinityCore - Confluence より


mingw-utils-0.3 (mingw-utils-0.3.tar.gz) を入手して、その中に入っている reimp.exe を使ってみたら、あっさり .lib から .a に変換できてしまった…。こんな罠があるとは…。

SDL 1.x関連の.libを変換 :

以下のSDL関連ファイルを入手して、中に入っていた .libを変換した。
SDL_image-devel-1.2.12-VC.zip
SDL_mixer-devel-1.2.12-VC.zip
SDL_net-devel-1.2.8-VC.zip
SDL_ttf-devel-2.0.11-VC.zip

reimp SDL_image.lib
reimp SDL_mixer.lib
reimp SDL_net.lib
reimp SDL_ttf.lib

変換すると、.a と .def が生成される。
  • .a がMinGW用ライブラリ。
  • .def は、dlltool というツール用のファイル。

dlltool に .defファイルを渡してやることで、.dll の各機能を呼び出すためのライブラリ(lib*.a)を生成できるらしい。今回は lib*.a が reimp で得られたから、.def は必要ないとは思うけど…。

_dllからインポートライブラリを作成する方法

SDL本体は公式配布版を使う :

SDL本体だけはMinGW用ライブラリが配布されていたので、ソレをそのまま使うことにした。
SDL-devel-1.2.15-mingw32.tar.gz

_SDL - Index of /release

以下の2つのファイルを使う。
libSDL.dll.a
libSDLmain.a

余談。前述のファイルを解凍すると、ファイル名の先頭が「.」になってるファイルがたくさん入ってた。たぶんコレ、Mac用じゃないかなと…。無視して構わない。

SDL_gfxだけはビルドした :

SDL_gfx は Visual C++用ライブラリすら無かったので、MinGW (gcc 6.3.0) を使ってビルドした。

本来は .a と .dll の両方が必要になるけれど、FreeBASIC 1.10.1 32bit版の lib/ には、標準状態でも、libSDL_gfx.dll.a だけは同梱されていた。だから、プロジェクトフォルダ内に SDL_gfx.dll だけ置いておけば SDL_gfx を使える。つまり今回は SDL_gfx.dll を得るためだけにビルドする。

_SDL_gfx / SDL2_gfx - ferzkopp.net

SDL_gfx-2.0.26.tar.gz を入手して解凍。

中に Makefile.mingw という、MinGW用の Makefile があるのでソレを使ってビルドする。ただ、何ヶ所か修正しないといけない。
  • prefix には MinGWインストールフォルダを指定しないといけない。…もしかして make に渡すオプションで変更できたりする?
  • SDL_gfxBlitFunc.o というファイルもコンパイルしてリンクしてやる。

> diff Makefile.mingw.orig Makefile.mingw -u
--- Makefile.mingw.orig Tue Dec 22 12:00:31 2009
+++ Makefile.mingw      Mon Jan 29 05:16:00 2024
@@ -2,7 +2,7 @@
 AR=ar rc
 RANLIB=ranlib

-prefix=c:/dev
+prefix=D:/MinGW
 bin_dir=$(prefix)/bin
 include_dir=$(prefix)/include
 lib_dir=$(prefix)/lib
@@ -10,7 +10,7 @@
 CFLAGS = -O3 -march=athlon-xp -mmmx -msse -m3dnow -DBUILD_DLL -DWIN32 -I$(include_dir)/SDL
 LIBS = -L$(lib_dir) -lSDL

-OBJS = SDL_framerate.o SDL_gfxPrimitives.o SDL_imageFilter.o SDL_rotozoom.o
+OBJS = SDL_framerate.o SDL_gfxBlitFunc.o SDL_gfxPrimitives.o SDL_imageFilter.o SDL_rotozoom.o

 STATIC_LIB=libSDL_gfx.a
 IMPORT_LIB=libSDL_gfx.dll.a
@@ -49,13 +49,14 @@

 install: $(STATIC_LIB) $(SHARED_LIB)
        cp $(STATIC_LIB) $(SHARED_LIB) $(IMPORT_LIB) $(lib_dir)
-       cp SDL_framerate.h SDL_gfxPrimitives.h SDL_imageFilter.h SDL_rotozoom.h $(include_dir)/SDL
+       cp SDL_framerate.h SDL_gfxBlitFunc.h SDL_gfxPrimitives.h SDL_imageFilter.h SDL_rotozoom.h $(include_dir)/SDL

 uninstall:
        rm -f $(lib_dir)/$(STATIC_LIB)
        rm -f $(lib_dir)/$(SHARED_LIB)
        rm -f $(lib_dir)/$(IMPORT_LIB)
        rm -f $(include_dir)/SDL/SDL_framerate.h
+       rm -f $(include_dir)/SDL/SDL_gfxBlitFunc.h
        rm -f $(include_dir)/SDL/SDL_gfxPrimitives.h
        rm -f $(include_dir)/SDL/SDL_imageFilter.h
        rm -f $(include_dir)/SDL/SDL_rotozoom.h

このあたりは以下に情報があった。

_windows 7 - SDL_gfx 2.0.24 DLL is not build, using MinGW - Stack Overflow

修正できたら、以下でビルド。
make -f Makefile.mingw

SDL_gfx.dll が生成されているので、FreeBASICでは、この .dll を使う。

MinGWから使う予定があるなら、MinGWにインストール。
make -f Makefile.mingw install

*1: という話だったけど今は状況が違ってたりする、という話もどこかで見かけた記憶もあって…。どこで目にしたのだったか…。

#2 [prog] SDL 1.xをMinGWでビルドしようとしたけれど途中で挫折した

Windows + FreeBASICで SDL 1.x を使うためには、MinGW用ライブラリファイル libsdl*.a が必要になる。しかし、SDLの公式配布版は、Visual C++用ライブラリファイルはあっても、MinGW用は配布していない。

であればと、ソースを入手して、MinGWでビルドできるか試してみた。環境は Windows10 x64 22H2 + MinGW (gcc 6.3.0)。

ただ、途中で手詰まりになって、結局目的のファイル群は得られなかった…。そこから reimp を使って .lib を .a に変換して使う方向に切り替えた。とは言え、途中まではビルドできたので、何かのヒントぐらいにはなるかもしれない。一応作業メモを残しておく。

余談。MSYS2 なら SDL 1.x も SLD 2.x もパッケージで用意されてるので、MSYS2 を使う分にはSDL関連をわざわざビルドする必要はないです。

configureのオプション :

configure を使って Makefile を作る際、渡すオプションが色々あるらしい…? ググっていて見かけたものを一応メモしておく。
./configure --prefix=/mingw --disable-shared --disable-assembly
./configure --prefix=/mingw --disable-shared
./configure --prefix=/mingw
./configure --build=i686-pc-mingw32 --disable-shared
./configure --host=i686-pc-mingw32 --disable-shared
./configure --prefix=/mingw --without-png

  • --prefix=/mingw は、MinGW用だよと指定しているのだろう。たぶん。
  • --disable-shared は、DLLを作らない指定なのでは。スタティックリンク用のライブラリだけを作れ、ということかなと…。
  • --disable-assembly は謎。
  • --build=i686-pc-mingw32 は、Windows 32bit版を作れという指定だろうか。
  • --without-png は、pngをサポートしない版を作れ、ということだろうか。

ソースの入手 :

以下のファイルを入手して解凍。ちなみに、これではまだ足りない。jpeg や tiff関連のファイルも必要なはずなので…。
SDL-1.2.15.tar.gz
freetype-2.7.1.tar.gz
SDL_ttf-2.0.11.tar.gz
SDL_net-1.2.7.tar.gz
smpeg-0.4.4.tar.gz
libogg-1.1.3.tar.gz
libvorbis-1.2.0.tar.gz
SDL_mixer-1.2.12.tar.gz
SDL_image-1.2.12.tar.gz
SDL_gfx-2.0.26.tar.gz

入手先は以下。

_SDL - Index of /release
_SDL_image - Index of /projects/SDL_image/release
_SDL_mixer - Index of /projects/SDL_mixer/release
_SDL_ttf - Index of /projects/SDL_ttf/release
_SDL_net - Index of /projects/SDL_net/release
_SDL_gfx / SDL2_gfx - ferzkopp.net
_SDL_gfx - Browse Files at SourceForge.net
_Index of /releases/freetype/
_smpeg - mirrors.dotsrc.org
_Xiph.org: Downloads
_Ftp - /pub/xiph/releases/ogg/ :: Oregon State University Open Source Lab
_Ftp - /pub/xiph/releases/vorbis/ :: Oregon State University Open Source Lab

ビルドに挑戦 :

MinGW が使える状態、かつ、configure を使う関係で、bash を起動して、その上で作業していった。

ちなみに、tra zxvf hoge.tar.gz と打てば解凍できるはずなので、事前に解凍しておく必要はなかったかもしれない…。

SDL 1.2 のビルド。
cd SDL-1.2.15
./configure --prefix=/mingw
make
make install


SDL_ttf は freetype2 を必要とするので、freetype2 をビルド。--without-png で、pngはサポートしなくて良い、と指定してる。らしい。
cd ../freetype-2.7.1
./configure --prefix=/mingw --without-png
make
make install

SDL_ttf をビルド。
cd ../SDL_ttf-2.0.11
./configure --prefix=/mingw
make
make install


SDL_net をビルド。--disable-gui とつけて、GUI関係を無効にしてるのだろうか。
cd ../SDL_net-1.2.7
./configure --prefix=/mingw --disable-gui
make
make install


SDL_mixer は、smpeg、libogg、libvorbis が必要と言う話を見かけた。smpeg をビルド。
cd ../smpeg-0.4.4
./configure --prefix=/mingw

Makefile ができているので、147行目を修正。最後に -lstdc++ を追加。
LIBS = -L/usr/lib -lmingw32 -lSDLmain -lSDL -mwindows -lm -lstdc++

make
make install

しかし、make すると、MPEGaudio.cpp で Play_MPEGaudioSDL や Decode_MPEGaudio が無いとエラーが出る。以下を参考にして修正。

_smpegのコンパイル

MPEGaudio.h の最後に以下を追加。
void Play_MPEGaudioSDL(void *udata, Uint8 *stream, int len);  
int Play_MPEGaudio(MPEGaudio *audio, Uint8 *stream, int len);  
#ifdef THREADED_AUDIO  
int Decode_MPEGaudio(void *udata);  
#endif

audio/huffmantable.cpp でもエラーが出る。
huffmantable.cpp:587:1: error: narrowing conversion of '-1' from 'int' to 'unsigned int' inside { } [-Wnarrowing]
huffmantable.cpp:587:1: error: narrowing conversion of '-1' from 'int' to 'unsigned int' inside { } [-Wnarrowing]
huffmantable.cpp:587:1: error: narrowing conversion of '-1' from 'int' to 'unsigned int' inside { } [-Wnarrowing]
huffmantable.cpp:587:1: error: narrowing conversion of '-1' from 'int' to 'unsigned int' inside { } [-Wnarrowing]
huffmantable.cpp:587:1: error: narrowing conversion of '-1' from 'int' to 'unsigned int' inside { } [-Wnarrowing]

const HUFFMANCODETABLE MPEGaudio::ht[HTN] の中で、unsigned int 値を書かなきゃいけないところで、マイナス値を書いてしまっている部分がある。

「0-1」を 「(unsigned int)(0-1)」としてみたら、コンパイルが通るようになった。

audio/.libs/ の中に、libaudio.a, libaudio.la が生成されたが、make install で MinGWインストールフォルダ/lib/ にコピーしてくれなかった。手作業でコピーしておいた。


liboggをビルド。
cd ../libogg-1.1.3
./configure --prefix=/mingw --disable-shared
make
make install


libvorbisをビルド。
cd ./libvorbis-1.2.0
./configure --prefix=/mingw --disable-shared
make
make install

configure を実行した段階で、oggが無いと言われてエラーになってしまった。ついさっき、libogg をインストールしたはずだけど…。おかしい…。

...
checking for pthread_create in -lpthread... yes
checking for pkg-config... yes
checking for pkg-config... /d/Perls/strawberry/5.32.1.1-x64/perl/bin/pkg-config
checking for ogg >= 1.0... checking for Ogg... no
*** Could not run Ogg test program, checking why...
*** The test program failed to compile or link. See the file config.log for the
*** exact error that occured. This usually means Ogg was incorrectly installed
*** or that you have moved Ogg since it was installed. In the latter case, you
*** may want to edit the ogg-config script:
configure: error: must have Ogg installed!

このあたりで手詰まり。

参考ページ :

原因が分かってきた :

ビルドできなかった原因が少し分かってきた。

原因その1。ビルド中に、MinGWが持ってないはずのツールが呼び出されてた。

例えば、pkg-config というツールを MinGW は持っていないのだけど、環境変数 PATH の中に登録されていた Strawberry Perl の bin/ の中に pkg-config が存在していて、configure を実行した際に「pkg-config…持ってるね!」と処理されてた。

一時的に Perlの入っているフォルダ名をリネームして参照されないようにしたところ、動作が少し変わった。

原因その2。素人考えで bash を単独で起動させて、その上で作業してたのが間違いだった。

msys.bat を実行して、その上で作業していけばすんなりビルドできた…。例えば、今までは make: write error と表示されていた場面でも、エラーが出ずに進むようになった。

以下のページを目にして、msys.bat の実行が絡んでそうと気づいた…。

_DirectSoundとRubyのプログラミング その7 - mirichiの日記


ということは、今まで行った作業も何か怪しいことになってる可能性があるなと…。SDL のビルドからやり直してみた。

しかし今度は、smpeg のビルドで大量のエラーが出る…。またしても手詰まり。

2024/01/30(火) [n年前の日記]

#1 [prog][windows] batファイルで環境変数PATH内の一部を無効化

環境は Windows10 x64 22H2。

自分の環境は、環境変数 PATH の中に、Strawberry Perl のインストールフォルダが登録されている。そのせいで、MinGW利用時、MinGW が持ってないはずのツールが ―― 例えば pkg-config などが、Strawberry Perl の bin/ の中から呼び出されてしまう。たぶん他にも、呼び出されたら困るツールがありそうな気もする。

MinGW を利用する時だけ、Strawberry Perl の bin/ を無効化したい。つまり、環境変数 PATH の中から、Strawberry Perl のインストールフォルダだけを除去したい。

文字列置換ができそう :

ググったら、以下が使えそうな気がした。

_バッチファイルの引数の文字列置換・パス展開 | 晴耕雨読
%PATH:str1=str2%
      変数内の文字列1を文字列2に置換

コレを使って、MinGW利用開始前に実行するbatファイル内で、Perlインストールディレクトリを無効化してみた。"Perls" を "_Perls" にしてしまうことで、Perlインストールディレクトリが参照されない状態にする。

set PATH=%PATH:Perls=_Perls%

件のbatファイルの全文は以下。

mingw.bat
@echo off
@rem MinGW,MSYS enable

@rem disable perl
set PATH=%PATH:Perls=_Perls%

@rem Please set your MinGW install Directory.
set MINGW_PATH=D:\MinGW

set MSYS_PATH=%MINGW_PATH%\\msys\1.0
set MINGWADDPATH=%MINGW_PATH%\bin;%MSYS_PATH%\bin
set PATH=%MINGWADDPATH%;%PATH%

set CPATH=%MINGW_PATH%\include
set C_INCLUDE_PATH=%MINGW_PATH%\include

@rem set CPLUS_INCLUDE_PATH=%MINGW_PATH%\include
set CPLUS_INCLUDE_PATH=%MINGW_PATH%\lib\gcc\mingw32\6.3.0\include\c++

set LIBRARY_PATH=%MINGW_PATH%\lib;%MINGW_PATH%\lib\gcc\mingw32\6.3.0

echo MinGW (SourceForge, gcc 6.3.0) enable. Add path [%MINGWADDPATH%]
echo.
echo CPATH              = %CPATH%
echo C_INCLUDE_PATH     = %C_INCLUDE_PATH%
echo CPLUS_INCLUDE_PATH = %CPLUS_INCLUDE_PATH%
echo LIBRARY_PATH       = %LIBRARY_PATH%

これで上手く行った気がする。mingw.bat を実行してから、which pkg-config と打っても何も表示されない。つまりは Perl のインストールフォルダが見えてない状態になってくれた。

#2 [prog] SDL_image 1.2をMinGWでビルドしてみたかった

Windows10 x64 22H2 + MinGW (gcc 6.3.0) で、SDL 1.x関連のライブラリファイルをビルドできないか試してる。

SDL_mixer は smpeg のビルドで詰まってしまったので、気分転換(?)で、SDL_image 1.2.12 をビルドできないか試してみることにした。

結果を先に書いておくと、SDL_image は tiff (libtiff) のビルドで詰まってしまった…。

要求されるライブラリのバージョン :

SDL_image 1.2.12 のリリース時のドキュメントに、依存してるライブラリのバージョンが書いてあった。

_SDL_image 1.2
SDL_image binaries have been built with the following libraries:
   JPEG v8d (jpegsr8d.zip)
   libpng 1.5.7 (lpng157.zip)
   libwebp 0.1.3 (libwebp-0.1.3.zip)
   tiff 4.0.0 (tiff-4.0.0.zip)
   zlib 1.2.5 (zlib125.zip)

バージョンは合わせておいたほうがいいのだろう…。

ただ、libwebp 0.1.3 だけはググっても入手できなかったので、比較的バージョンが低いものを選ぶことにした。

ソースファイルの入手 :

ビルド作業 :

zlib, libpng, jpeg, libwebp, tiff の順番でビルドしていく。

msys.bat を実行して、MinGW を使えるようにしておく。msys.bat を実行した上で作業しないと、ビルドできたはずのものも、ビルドできなくなる…。

zlib,libpngをビルド :

zlib と libpng のビルドは以下が参考になった。ありがたや。

_zlib-1.2.7をMinGWでビルドする - 新・日々録 by TRASH BOX@Eel
_libpng-1.2.49、libpng-1.5.10をMinGWでビルドする - 新・日々録 by TRASH BOX@Eel
_zlibのコンパイル&インストール お気に入りの動画を携帯で見よう

zlib をビルド。
tar zxvf zlib-1.2.5.3.tar.gz
cd zlib-1.2.5.3
make -f win32/Makefile.gcc all

生成されたファイルを手作業でコピー。
  • libz.a, libz.dll.a は /MinGW/lib/ へコピー。
  • zconf.h, zlib.h は /MinGW/include/ へコピー。
  • zlib1.dll は /MinGW/bin/ へコピー。


libpng をビルド。
cd ..
tar zxvf libpng-1.5.7.tar.xz
cd libpng-1.5.7
make -f scripts/makefile.gcc

生成されたファイルを手作業でコピー。
  • libpng.a は /MinGW/lib/ へコピー。
  • png.h, pngconf.h, pnglibconf.h は /MinGW/include/ へコピー。
  • pngdebug.h, pnginfo.h, pngpriv.h, pngstruct.h も念のために /MinGW/include/ へコピーしておいた。

jpegをビルド :

以下が参考になった。

_Routine-Work:/resources/doc/jpeg/install-mingw32-02.html (WebArchive)

jpeg-6b の時点では C++ に対応させるために patch を当てていたようだけど、jpeg-8d のソースを眺めたら patch を当てたものと似た感じの記述になっていたので、おそらくソースをそのまま利用できるだろうと踏んで作業した。

cd ..
tar zxvf jpegsrc.v8d.tar.gz
cd jpeg-8d
./configure --prefix=/mingw
make
make install

以下の場所にファイルがコピーされた。他にもあるかもしれないけど…。
/MinGW/bin/libjpeg-8.dll
/MinGW/bin/cjpeg.exe
/MinGW/bin/djpeg.exe
/MinGW/bin/jpegtran.exe
/MinGW/bin/rdjpgcom.exe
/MinGW/bin/wrjpgcom.exe
/MinGW/include/jconfig.h
/MinGW/include/jerror.h
/MinGW/include/jmorecfg.h
/MinGW/include/jpeglib.h
/MinGW/lib/libjpeg.a
/MinGW/lib/libjpeg.dll.a
/MinGW/lib/libjpeg.la

libwebpのビルド :

以下を参考にして作業。

_libwebpのコンパイル&インストール お気に入りの動画を携帯で見よう

cd ..
tar zxvf libwebp-0.6.1.tar.gz
cd libwebp-0.6.1
./configure --prefix=/mingw
make
make install

以下の場所にファイルがコピーされた。他にもあるかも。
/MinGW/bin/libwebp-7.dll
/MinGW/bin/cwebp.exe
/MinGW/bin/dwebp.exe
/MinGW/include/webp/decode.h
/MinGW/include/webp/encode.h
/MinGW/include/webp/types.h
/MinGW/lib/libwebp.a
/MinGW/lib/libwebp.dll.a
/MinGW/lib/libwebp.la

libtiffのビルド :

以下を参考にして作業。

_Routine-Work:/resources/doc/libtiff/install-mingw32-02.html (WebAechive)

cd ..
tar zxvf tiff-4.0.0.tar.gz
cd tiff-4.0.0
./configure --prefix=/mingw
make
make install

make をした段階で、大量のエラーが出た。どうもリンクが上手く行ってないように見える。lib/libstdc++.a がどうとか表示されてる。

ここで手詰まり。

もしかして、gcc 6.3.0 を使っているから失敗するのだろうか。ググったところ、この時期のSDL関連ファイルのビルドには、MinGW 5.1.4 (gcc 3.4.5) を使っていた事例が多いようだし…。

SDL_imageのビルド :

libtiff はビルドできなかったけど、以下のページを眺めたところ、tiff を無効にした状態で SDL_imageをビルドできなくもないらしい。試してみることにした。

_MinGW + SDL 環境の構築

cd ..
tar zxvf SDL_image-1.2.12.tar.gz
cd SDL_image-1.2.12
./configure --prefix=/mingw --disable-tif
make
make install

一応、以下にファイルがコピーされた。
/MinGW/bin/SDL_image.dll
/MinGW/include/SDL/SDL_image.h
/MinGW/lib/libSDL_image.a
/MinGW/lib/libSDL_image.dll.a
/MinGW/lib/libSDL_image.la

libtiffをビルドできてない点が気になるけれど…。

tiffのバージョンを変えて試した :

tiff (libtiff) のバージョンを変えて、ビルドできるか試してみた。

_Index of /libtiff/

  • tiff 4.0.0, 4.0.1 ... ビルド失敗
  • tiff 4.0.4, 4.0.8, 4.6.0 ... ビルド成功
どうやらバージョンを上げていくとビルドできるようだなと…。

ただ、SDL_image 1.2.12 で、./configure --prefix=/mingw と打った際、tiff関係で警告が出ているように見えた。
checking for LIBPNG... no
checking png.h usability... yes
checking png.h presence... yes
checking for png.h... yes
checking for png_create_read_struct in -lpng... yes
checking tiffio.h usability... yes
checking tiffio.h presence... yes
checking for tiffio.h... yes
checking for TIFFClientOpen in -ltiff... no
configure: WARNING: *** Unable to find Tiff library (http://www.remotesensing.org/libtiff/)
configure: WARNING: TIF image loading disabled
checking for LIBWEBP... yes

これってどうなんだろう…。TIF image loading disabled って言ってるけれど…。結局、tiff が無効になってるということ?

#3 [nitijyou] 腹痛がツライ

夕方頃、左の背中から左脇腹にかけてズキズキする痛み。たぶんまた結石だろう、我慢するしかないと、痛み止めのロキソニンを飲んだ。振動で落ちてくれないかと、無駄にジャンプを繰り返したり、犬の散歩に行ってみたりしたけれど…。4時間経っても痛みが治まらない。仕方なく、二錠目のロキソニンを飲んだ。一日に二錠も飲むのは初めてのような気がする。ツライ…。明日までには痛みが消えてくれるだろうか…。

一応メモ。ロキソニンは、飲んでから15分〜60分経った頃に効き始める、と書いてあった気がする。一錠飲んだら次に飲めるのは4時間後。また、一日に飲めるのは基本的に2回まで、と説明書には書いてあった。腎臓への負担を考えたら常用するのは良くないらしい。

2024/01/31(水) [n年前の日記]

#1 [basic] FreeBASICでTIC-80風のHelloWorld

Windows10 x64 22H2 + FreeBASIC 1.10.1 を触ってるけれど、せっかくだから TIC-80風の Hello World を書いてみようかなと思いついた。

TIC-80 は Fantsy Console (Fatntasy Computer)の一種。テキストエディタ、ドットエディタ、マップエディタ、サウンドエディタ、ミュージックエディタが、1ファイルの中に全部入っているので、すぐにゲーム制作が始められる。

このジャンルのアプリでは、PICO-8(有償アプリ)が有名だけど、TIC-80は無料で利用できる。(有償版もある。)

_TIC-80 tiny computer

その TIC-80 は、起動直後にサンプルファイルが既にロードされてる状態で始まるのだけど。そのサンプルと似たような処理を書けるなら、ひとまず FreeBASIC でも、2Dピコピコゲーム程度なら書けそうだ、と分かるのではないかなと…。

サンプルソース :

そんなわけで書いてみた。




_helloliketic80.bas
' game main loop

#ifdef __FB_WIN32__

' Windowsの場合、mmsystemを利用
#include "windows.bi"
#include "win/mmsystem.bi"

#endif

' 画像読み込み用ライブラリ FBImage を使う場合は以下のコメントアウトを外す
' #include once "FBImage.bi"

' fbgfxモードを使う
#Include "fbgfx.bi"
Using fb

' 時間計測用の変数
Dim As Double start_time, prev_time, now_time, delta, one_frame, next_time
Dim As Integer frame_count
Dim As String fps_text = "FPS"

Dim As Integer scrw, scrh  ' ウインドウサイズ
Dim As Integer imgw, imgh  ' 画像サイズ

' カレントディレクトリを exeファイルのある場所にする
chdir exepath()

' ウインドウサイズと色深度を指定
scrw = 512
scrh = 288
Screenres scrw, scrh, 32

' 画像読み込み
#ifdef __FBImage_bi__

' FBImageを利用して読み込む場合
var img = LoadRGBAFile("obj.png")

#Else

' FreeBASIC標準のbmp読み込みを使う場合
Dim img As any ptr = ImageCreate(128, 64)
Bload "obj.bmp", img

#endif

' 画像の幅と高さを取得
imageinfo img, imgw, imgh

#ifdef __FB_WIN32__
' タイマー精度を1msecに向上
timeBeginPeriod(1)
#endif

' 1フレームあたりの本来の時間
Dim As Double MAX_FPS = 60.0
one_frame = 1.0 / MAX_FPS

' 開始時間を取得
start_time = Timer
prev_time = start_time
frame_count = 0

Dim As Boolean running = True
Dim As Double x, y
x = scrw / 2
y = scrh / 2

Dim As Double anime_t = 0.0

' メインループ
While (running)

    ' 前回フレームから何秒経過したか取得。単位は秒(小数点以下有り)
    now_time = Timer
    If now_time >= prev_time Then
        delta = now_time - prev_time
    Else
        delta = one_frame
    End If
    prev_time = now_time
    next_time = now_time + one_frame

    If now_time >= start_time Then
        If (now_time - start_time) >= 1.0 Then
            ' 1秒経過したのでFPSを取得
            fps_text = "FPS: " & frame_count
            start_time += 1.0
            frame_count = 0
        End If
    Else
        start_time = now_time
    End If

    ' ESCキー、qキー、ウインドウの閉じるボタンを検出
    Dim As String k = inkey$
    If k = Chr$(27) Or k = "q" Or k = Chr$(255) + "k" Then
        ' メインループ終了
        running = False
    End If

    ' キャラを移動
    Dim As Double spd = 4.0 * MAX_FPS * delta
    If MultiKey(SC_LEFT ) Then x -= spd
    If MultiKey(SC_RIGHT) Then x += spd
    If MultiKey(SC_UP   ) Then y -= spd
    If MultiKey(SC_DOWN ) Then y += spd

    ' アニメ表示用カウンタを更新
    anime_t += delta

    ' 描画開始
    ScreenLock

    ' 画面クリア
    color RGB(255, 255, 255), RGB(52, 164, 255)
    cls

    ' 画像を描画
    Dim As Integer n, sx, sy, sw, sh
    n = Int(anime_t / 0.3) Mod 2  ' 0 or 1
    sw = (imgw / 2)  ' 幅
    sh = imgh        ' 高さ
    sx = sw * n      ' 描画元 x
    sy = 0           ' 描画元 y
    Put (x - (sw / 2), y - (sh / 2)), img, (sx, sy) - Step(sw, sh), TRANS

    ' 文字列を描画
    Draw String (10, 10), fps_text
    Draw String (scrw / 2 - (8 * 6), scrh * 0.8), "HELLO WORLD"

    ' 描画終了
    ScreenUnlock

    If Timer < next_time Then
        ' 本来の1フレーム時間がまだ経過してないので sleep させる
        Dim As Double wait_ms = (next_time - Timer) * 1000.0
        If wait_ms > 0.0 Then sleep wait_ms
    End If

    frame_count += 1
Wend

' キーバッファを空にする
While Inkey <> "": Wend

#ifdef __FB_WIN32__
' タイマー精度を本来のスペックに戻す
timeEndPeriod(1)
#endif

' 画像を使い終わったので破棄
ImageDestroy img

使っている画像は以下。

_obj.bmp
_obj.png

少し解説 :

TIC-80 のサンプルと比べたら、無駄にソースが長くなるなと…。やってることは同じなのに…。ゲーム制作に特化した環境と、どんなアプリも作れる汎用性を重視している環境では、こういうところで違いが出てくるのかもしれない。

さておき。FreeBASIC でキー入力を検出するためには、一般的には Inkey を使うけれど、カーソルキー押しっぱなしでキャラを移動させたりする時は MultiKey() を使う。この MultiKey() を使う時は、fbgfxモードとやらを有効にしないといけないらしい。
' fbgfxモードを使う
#Include "fbgfx.bi"
Using fb

' ...

    ' キャラを移動
    Dim As Double spd = 4.0 * MAX_FPS * delta
    If MultiKey(SC_LEFT ) Then x -= spd
    If MultiKey(SC_RIGHT) Then x += spd
    If MultiKey(SC_UP   ) Then y -= spd
    If MultiKey(SC_DOWN ) Then y += spd


FreeBASICで時間を取得したい時は、Timer を使う。単位は秒で、小数点以下の値も入ってくるから、ミリ秒も測定できる。ほとんどの場合、PCが起動してからの時間を返してくるけれど、動かしてるOSによってどのあたりが開始時間になるかは異なる。
    ' 前回フレームから何秒経過したか取得。単位は秒(小数点以下有り)
    now_time = Timer
    If now_time >= prev_time Then
        delta = now_time - prev_time
    Else
        delta = one_frame
    End If
    prev_time = now_time
    next_time = now_time + one_frame


Windows上で FreeBASIC を動かした際、標準状態だと sleep の精度が荒いけれど、1ミリ秒の精度にしたいときは、マルチメディア関連の機能を使う。Linux の場合は標準状態で1ミリ秒の精度なので気にしなくていい。
#ifdef __FB_WIN32__

' Windowsの場合、mmsystemを利用
#include "windows.bi"
#include "win/mmsystem.bi"

#endif

' ...

#ifdef __FB_WIN32__
' タイマー精度を1msecに向上
timeBeginPeriod(1)
#endif

' ...

#ifdef __FB_WIN32__
' タイマー精度を本来のスペックに戻す
timeEndPeriod(1)
#endif


FreeBASIC は標準状態だと bmp画像の読み込みしかできない。png画像も読み込みたい時は、何かしらの画像ライブラリを使うことになる。今回は FBImage という画像ライブラリを使って実験していた。
' 画像読み込み用ライブラリ FBImage を使う場合は以下のコメントアウトを外す
' #include once "FBImage.bi"

' ...

' 画像読み込み
#ifdef __FBImage_bi__

' FBImageを利用して読み込む場合
var img = LoadRGBAFile("obj.png")

#Else

' FreeBASIC標準のbmp読み込みを使う場合
Dim img As any ptr = ImageCreate(128, 64)
Bload "obj.bmp", img

#endif

' 画像の幅と高さを取得
imageinfo img, imgw, imgh

' ...

' 画像を使い終わったので破棄
ImageDestroy img

FBImage のインストールは、以前の日記を参考に。

_FreeBASICで画像描画 - mieki256's diary


画像描画は Put を使う。文字の描画は Draw String を使う。


描画開始時に ScreenLock を呼んで、描画が終わったら ScreenUnlock を呼べば、画面のちらつきを防止できる、という話を見かけたので一応やっている。

昔のBASICらしい画面にしてみた :

前述のように、一応書けたのだけど。これってどうも、「BASICで書いてますよー」感が弱いなと…。

もうちょっと手直ししてみた。

_helloliketic80ascii.bas
' game main loop

#ifdef __FB_WIN32__

' Windowsの場合、mmsystemを利用
#include "windows.bi"
#include "win/mmsystem.bi"

#endif

' fbgfxモードを使う
#Include "fbgfx.bi"
Using fb

' 時間計測用の変数
Dim As Double start_time, prev_time, now_time, delta, one_frame, next_time
Dim As Integer frame_count
Dim As String fps_text = "FPS"

Dim As Integer scrw, scrh  ' ウインドウサイズ

' ウインドウサイズと色深度を指定
scrw = 512
scrh = 288
Screenres scrw, scrh, 32

#ifdef __FB_WIN32__
timeBeginPeriod(1) ' タイマー精度を1msecに向上
#endif

' 1フレームあたりの本来の時間
Dim As Double MAX_FPS = 60.0
one_frame = 1.0 / MAX_FPS

' 開始時間を取得
start_time = Timer
prev_time = start_time
frame_count = 0

Dim As Boolean running = True
Dim As Double x, y
x = scrw / 2
y = scrh / 2

Dim As Double anime_t = 0.0

' メインループ
While (running)

    ' 前回フレームから何秒経過したか取得。単位は秒(小数点以下有り)
    now_time = Timer
    If now_time >= prev_time Then
        delta = now_time - prev_time
    Else
        delta = one_frame
    End If
    prev_time = now_time
    next_time = now_time + one_frame

    If now_time >= start_time Then
        If (now_time - start_time) >= 1.0 Then
            ' 1秒経過したのでFPSを取得
            fps_text = "FPS: " & frame_count
            start_time += 1.0
            frame_count = 0
        End If
    Else
        start_time = now_time
    End If

    ' ESCキー、qキー、ウインドウの閉じるボタンを検出
    Dim As String k = inkey$
    If k = Chr$(27) Or k = "q" Or k = Chr$(255) + "k" Then
        ' メインループ終了
        running = False
    End If

    ' キャラを移動
    Dim As Double spd = 4.0 * MAX_FPS * delta
    If MultiKey(SC_LEFT ) Then x -= spd
    If MultiKey(SC_RIGHT) Then x += spd
    If MultiKey(SC_UP   ) Then y -= spd
    If MultiKey(SC_DOWN ) Then y += spd

    ' アニメ表示用カウンタを更新
    anime_t += delta

    ' 描画開始
    ScreenLock

    ' 画面クリア
    color RGB(255, 255, 255), RGB(52, 164, 255)
    cls

    ' キャラクターを描画
    Dim As Integer n = Int(anime_t / 0.5) Mod 2  ' 0 or 1
    If n = 0 Then
        Draw String (x, y + 8 * 0), "+-----+"
        Draw String (x, y + 8 * 1), ":O   O:"
        Draw String (x, y + 8 * 2), ":     :"
        Draw String (x, y + 8 * 3), ": --- :"
        Draw String (x, y + 8 * 4), "+-----+"
        Draw String (x, y + 8 * 5), " H   H "
    Else
        Draw String (x, y + 8 * 0), "+-----+"
        Draw String (x, y + 8 * 1), ":-   -:"
        Draw String (x, y + 8 * 2), ":     :"
        Draw String (x, y + 8 * 3), ":  -  :"
        Draw String (x, y + 8 * 4), "+-----+"
        Draw String (x, y + 8 * 5), " H   H "
    End If

    ' 文字列を描画
    Draw String (10, 10), fps_text
    Draw String (scrw / 2 - (8 * 6), scrh * 0.8), "HELLO WORLD"

    ' 描画終了
    ScreenUnlock

    If Timer < next_time Then
        ' 本来の1フレーム時間がまだ経過してないので sleep させる
        Dim As Double wait_ms = (next_time - Timer) * 1000.0
        If wait_ms > 0.0 Then sleep wait_ms
    End If

    frame_count += 1
Wend

' キーバッファを空にする
While Inkey <> "": Wend

#ifdef __FB_WIN32__
timeEndPeriod(1)  ' タイマー精度を本来のスペックに戻す
#endif




よし。見た目がBASICらしくなった。

いや、自分、最初に使ってたPCが、グラフィック画面が無くて文字しか表示できない MZ-700 だったもんで…。PC-8801 や FM-7 や X1 使ってた人の「BASIC感」は、たぶん違うんだろうなあ…。

#2 [prog] smpegをMinGWでビルドできるか再挑戦してみたけど挫折した

Windows10 x64 22H2 + MinGW (gcc 6.3.0) で、SDL 1.x関連ライブラリをビルドできないものか試している。

先日は smpeg 0.4.4 のビルドに失敗して SDL_mixer もビルドできなかったけど。ググってたら、smpeg_0.4.5+cvs20030824.orig.tar.gz を入手できた。

_0.4.5+cvs20030824-8build1 : smpeg package : Ubuntu

smpeg 0.4.4 はビルドできなかったけど、0.4.5 ならビルドできるのではないか。試してみる。

smpeg 0.4.5のビルドに挑戦 :

msys.bat を実行して、その上で作業する。

tar zxvf smpeg_0.4.5+cvs20030824.orig.tar.gz
cd smpeg-0.4.5+cvs20030824.orig
./configure --prefix=/mingw --disable-opengl-player --enable-mmx --disable-gtk-player --disable-gtktest

続けて make を打つけれど、その前にいくつかのファイルを修正しないといけない。


_MinGW + SDL 環境の構築

生成された Makefile の147行目を修正。
LIBS =  -L/mingw/lib -lmingw32 -lSDLmain -lSDL -mwindows -lm
↓
LIBS =  -L/mingw/lib -lmingw32 -lSDLmain -lSDL -mwindows -lm -lstdc++


_smpegのコンパイル

MPEGaudio.h の最後に以下を追加。
void Play_MPEGaudioSDL(void *udata, Uint8 *stream, int len);
int Play_MPEGaudio(MPEGaudio *audio, Uint8 *stream, int len);
#ifdef THREADED_AUDIO
int Decode_MPEGaudio(void *udata);
#endif


audio/huffmantable.cpp を修正。

_Package: smpeg | Debian Sources

> diff -u huffmantable.cpp.orig huffmantable.cpp
--- huffmantable.cpp.orig       Thu Aug 26 13:37:52 1999
+++ huffmantable.cpp    Tue Jan 30 23:41:55 2024
@@ -9,6 +9,7 @@
 #include "config.h"
 #endif

+#include <climits>
 #include "MPEGaudio.h"

 static const unsigned int
@@ -550,11 +551,11 @@

 const HUFFMANCODETABLE MPEGaudio::ht[HTN]=
 {
-  { 0, 0-1, 0-1, 0,  0, htd33},
+  { 0, UINT_MAX, UINT_MAX, 0,  0, htd33},
   { 1, 2-1, 2-1, 0,  7,htd01},
   { 2, 3-1, 3-1, 0, 17,htd02},
   { 3, 3-1, 3-1, 0, 17,htd03},
-  { 4, 0-1, 0-1, 0,  0, htd33},
+  { 4, UINT_MAX, UINT_MAX, 0,  0, htd33},
   { 5, 4-1, 4-1, 0, 31,htd05},
   { 6, 4-1, 4-1, 0, 31,htd06},
   { 7, 6-1, 6-1, 0, 71,htd07},
@@ -564,7 +565,7 @@
   {11, 8-1, 8-1, 0,127,htd11},
   {12, 8-1, 8-1, 0,127,htd12},
   {13,16-1,16-1, 0,511,htd13},
-  {14, 0-1, 0-1, 0,  0, htd33},
+  {14, UINT_MAX, UINT_MAX, 0,  0, htd33},
   {15,16-1,16-1, 0,511,htd15},
   {16,16-1,16-1, 1,511,htd16},
   {17,16-1,16-1, 2,511,htd16},


ようやく make。
make

しかし、アセンブラソース、video/mmxflags_asm.S のところでエラーが出た。
$ make
Making all in audio
make[1]: Entering directory `/d/home/prg/c_lang/libs/sdl/build/smpeg-0.4.5+cvs20030824.orig/audio'
make[1]: Nothing to be done for `all'.
make[1]: Leaving directory `/d/home/prg/c_lang/libs/sdl/build/smpeg-0.4.5+cvs20030824.orig/audio'
Making all in video
make[1]: Entering directory `/d/home/prg/c_lang/libs/sdl/build/smpeg-0.4.5+cvs20030824.orig/video'
/bin/sh ../libtool --mode=compile gcc -DPACKAGE_NAME=\"\" -DPACKAGE_TARNAME=\"\" -DPACKAGE_VERSION=\"\" -DPACKAGE_STRING=\"\" -DPACKAGE_BUGREPORT=\"\" -DPACKAGE=\"smpeg\" -DVERSION=\"0.4.5\" -Dsocklen_t=int  -I. -I.      -g -O2 -I/mingw/include/SDL -D_GNU_SOURCE=1 -Dmain=SDL_main -DUSE_MMX -DTHREADED_AUDIO -DNDEBUG -I.. -DNOCONTROLS -I.. -I../audio -I../video -c mmxflags_asm.S
gcc -DPACKAGE_NAME=\"\" -DPACKAGE_TARNAME=\"\" -DPACKAGE_VERSION=\"\" -DPACKAGE_STRING=\"\" -DPACKAGE_BUGREPORT=\"\" -DPACKAGE=\"smpeg\" -DVERSION=\"0.4.5\" -Dsocklen_t=int -I. -I. -g -O2 -I/mingw/include/SDL -D_GNU_SOURCE=1 -Dmain=SDL_main -DUSE_MMX -DTHREADED_AUDIO -DNDEBUG -I.. -DNOCONTROLS -I.. -I../audio -I../video -c mmxflags_asm.S -o mmxflags_asm.o
mmxflags_asm.S: Assembler messages:
mmxflags_asm.S:6: Warning: .type pseudo-op used outside of .def/.endef ignored.
mmxflags_asm.S:6: Error: junk at end of line, first unrecognized character is `f'
mmxflags_asm.S:12: Warning: .type pseudo-op used outside of .def/.endef ignored.
mmxflags_asm.S:12: Error: junk at end of line, first unrecognized character is `c'
mmxflags_asm.S:57: Warning: .size pseudo-op used outside of .def/.endef ignored.
mmxflags_asm.S:57: Error: junk at end of line, first unrecognized character is `c'
make[1]: *** [mmxflags_asm.lo] Error 1
make[1]: Leaving directory `/d/home/prg/c_lang/libs/sdl/build/smpeg-0.4.5+cvs20030824.orig/video'
make: *** [all-recursive] Error 1

手詰まり。すんなり行かなかったな…。

再度 smpeg 0.4.4のビルドに挑戦 :

cd ..
tar zxvf smpeg-0.4.4.tar.gz
cd smpeg-0.4.4
./configure --prefix=/mingw --disable-opengl-player --disable-gtk-player --disable-gtktest

smpeg 0.4.5 の時と同じ修正作業をする。
  • Makefile を修正。
  • MPEGaudio.h を修正。
  • audio/huffmantable.cpp を修正。

make してみたけど、以前と同様にダメだった。大量にエラーが出る。
D:/MinGW/lib/libstdc++.a(new_op.o):new_op.cc:(.text+0x34): undefined reference to `_Unwind_SjLj_Register'
D:/MinGW/lib/libstdc++.a(new_op.o):new_op.cc:(.text+0x87): undefined reference to `_Unwind_SjLj_Unregister'
D:/MinGW/lib/libstdc++.a(new_op.o):new_op.cc:(.text+0xb7): undefined reference to `_Unwind_SjLj_Resume'
...
D:/MinGW/lib/libstdc++.a(eh_personality.o):eh_personality.cc:(.text+0x461): undefined reference to `_Unwind_SjLj_Register'
D:/MinGW/lib/libstdc++.a(eh_personality.o):eh_personality.cc:(.text+0x5d4): undefined reference to `_Unwind_SjLj_Unregister'
D:/MinGW/lib/libstdc++.a(eh_personality.o):eh_personality.cc:(.text+0x6c6): undefined reference to `_Unwind_SjLj_Unregister'

やはり MinGW (gcc 6.3.0) のバージョンが合ってないのだろうか?

#3 [prog] MinGW 5.1.4のインストールを試みた

smpeg 0.4.4 を Windows10 x64 22H2 + MinGW (gcc 6.3.0)上でビルドできない。古いバージョンの MinGW ならビルドできるのか気になってきたので、あえて MinGW 5.1.4 + msys 1.0.11 をインストールしてみた。

ファイルを入手 :

インストール :

今回は、D:/MinGW514/ というフォルダを作成して、その中に、MinGW/ と msys/ というフォルダを作成。そこに、MinGW 5.1.4 と msys 1.0.11 をインストールしてみた。

MinGW-5.1.4.exe を実行すると、ウイザード形式で色々尋ねてきて、そのうちインストールフォルダも尋ねてくる。D:\MinGW514\MinGW\ を指定してインストールした。最後に、ネットから必要なパッケージを自動でダウンロードして、自動で解凍してインストールしてくれた。

MSYS-1.0.11-2004.04.30-1.exe を実行すると、これもウイザード形式で色々尋ねてくる。インストール場所と、MinGW のインストール場所も尋ねてきた。

初期設定 :

インストール後、msys/1.0/etc/ 内で、fstab.sample を fstab にリネームコピー。fstab を編集して、/mingw の場所を指定し直し。
#Win32_Path     Mount_Point
D:/MinGW514/MinGW                           /mingw
D:/Perls/strawberry/5.32.1.1-x64/perl       /perl
#c:/ActiveState/perl        /perl

msys.batを実行 :

msys.bat を実行すると、rxvt が開く。

余談。MSYS-1.0.11-2004.04.30-1.exe に入っている、rxvt の初期サイズやフォントサイズは、msys.bat の最後のほうで、コマンドラインオプションを指定する形で設定されているけれど。「-fn Terminal-18」と言った感じでフォント種類を変更しようとしても、各文字の間に妙な空白が入った状態で表示されてしまう。どうやら、 デフォルト設定の -fn Courier-18 というフォント種類しか受け付けないようだなと…。MSYS 1.10.10 版ならそういうことにはならないそうだけど、探しても見つからなかった。

_MinGW及びMSYS - Hirotake Itoh's memo by PukiWiki

SDLのビルド :

SDL-1.2.15.tar.gz をビルドしてみようとしたけれど…。
tar zxvf SDL-1.2.15.tar.gz
cd SDL-1.2.15
./configure --prefix=/mingw

$ ./configure --prefix=/mingw
checking build system type... i686-pc-mingw32
checking host system type... i686-pc-mingw32
checking for gcc... gcc
checking for C compiler default output file name... 
configure: error: C compiler cannot create executables
See `config.log' for more details.

処理が進まない。Cコンパイラは実行形式を作れない、と言ってきた。

すんなりビルドできるのであれば、と思って試用してみたけれど、いきなり最初から躓いてしまった。

バージョンも色々古いから、他の作業で使うとも思えないし…。コントロールパネル経由でアンインストールしておいた。残念。

余談 :

これ、何度も書くけど。MSYS2 なら、SDL 1.x も SDL2 もパッケージになって用意されていて、例えば以下のような感じで SDL_mixer だろうと smpeg だろうとインストールできるから苦労しないです。SDL 1.x や SDL2 を使って何か作りたいなら、MSYS2 を使えばOKです。
pacman -S mingw-w64-i686-SDL_mixer
pacman -S mingw-w64-i686-smpeg

FreeBASIC で SDL 1.x を使いたい時も、reimp を使って、Visual C++用ライブラリ(.lib)を MinGW用ライブラリ(.a)に変換すれば、ひとまず使えちゃうので…。SDL2 なら、MinGW用ライブラリファイルも公式に配布されてるので…。

単に、今現在入手できる MinGW (gcc 6.3.0) で SDL 1.x 関係のビルドってできるのかな、難しいのかな、と気になってやってるだけなので…。

とりあえず、tiff (libtiff) と smpeg がビルドできないけれど、それ以外ならビルドできたので、機能を一部持ってない SDL_image と SDL_mixer でもいいなら、作れそうではあるなと…。実験してみて、そこまでは分かった…。

以上、31 日分です。

過去ログ表示

Prev - 2024/01 - Next
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

カテゴリで表示

検索機能は Namazu for hns で提供されています。(詳細指定/ヘルプ


注意: 現在使用の日記自動生成システムは Version 2.19.6 です。
公開されている日記自動生成システムは Version 2.19.5 です。

Powered by hns-2.19.6, HyperNikkiSystem Project