KDP(電子出版)のメモ 急急如律令

Amazon Kindleダイレクト・パブリッシングでの電子出版や電子書籍の作成販売について、文章やイラストの作成や編集方法について書いています。

蛍光灯からLED照明へ交換して気づいたこと。明るさ・色味・演色性で選ぶポイント

蛍光灯の製造・輸出入が規制されることを見据えて、照明器具が壊れたタイミングで少しずつLED照明に交換している。

実際にLED照明を選んでみると、価格や明るさだけでなく、使い勝手や光の色にも気になる点がいくつかあった。今回は、LED照明に交換する際に気をつけたいポイントをまとめていく。

紐で引っ張るタイプとリモコンタイプ、どちらがいい?

照明器具を選ぶとき、最初に気になったのが操作方法だ。

昔ながらの紐を引っ張って点灯・消灯するタイプは、部屋に入ったときにスイッチの位置がわかりやすい。壁のスイッチを探す必要がなく、照明器具から伸びている紐を引けば操作できるのは便利だ。

一方、紐で操作するペンダントライトは、製品によっては価格が高めになる。

それに対して、リモコンで操作するタイプは比較的安価な製品が多く、選択肢も豊富だ。明るさや光の色を調整できる製品もあり、機能面でも魅力がある。

ただし、リモコンは置き場所を決めておかないと、必要なときに見つからなくなることもある。紐で操作できるわかりやすさと、リモコンタイプの機能性や価格を比較して選びたい。

LED照明に交換して気になった5つのこと

1. 光が青っぽく、ギラついて見える

LED照明に交換して気になったのが、光の青っぽさやギラつきだ。

LED照明には、昼光色のような青白い光から、電球色のような暖かみのある光まで、さまざまな色味がある。製品によって光の見え方も異なるため、単純に明るければよいというわけではない。

青白い光が好みなら問題ないが、落ち着いた雰囲気の部屋にしたい場合や、暖かみのある光を好む場合には、光の色を確認してから購入したほうがよい。

また、光源が直接目に入ると、まぶしさが気になることもある。光の色だけでなく、カバーの形状や光の広がり方にも注目したい。

2. 天井付近が暗くなる

照明器具を交換して気づいたのが、天井付近の明るさの違いだ。

照明器具によっては、下方向には十分な光が届いていても、天井方向にはほとんど光が広がらないものがある。その場合、部屋の床や机は明るくても、天井付近が暗く感じられる。

部屋全体を明るく見せたいなら、下方向だけでなく、光がどの方向に広がるのかも確認しておきたい。

特に、天井に光を反射させて部屋全体を明るく見せたい場合は、天井方向にも光が届く設計の照明器具を選ぶことが重要だ。

3. 光の色を変更できない製品がある

蛍光灯タイプの照明器具では、対応する蛍光ランプを交換することで、光の色を変えられる場合がある。

一方、LED照明は、製品によって光の色が固定されている。電球を交換するような感覚で色味を変更できるとは限らない。

そのため、購入時には昼光色・昼白色・電球色など、どのような光の色に対応しているかを確認しておきたい。

リモコン付きの調色機能がある製品なら、作業するときは白っぽい光、くつろぐときは暖かみのある光というように、用途に合わせて切り替えられる。

後から光の色を変えたくなる可能性があるなら、最初から調色機能付きの製品を選んでおくと便利だ。

4. 実際の明るさは設置してみないとわかりにくい

LED照明を選ぶときは、明るさを示すルーメン(lm)という数値が参考になる。

ただし、同じ明るさの数値でも、照明器具の形状や光の広がり方によって、部屋での感じ方は変わる。

光が一方向に集中する製品と、部屋全体に広がる製品では、同じ数値でも印象が異なる。また、天井や壁の色、部屋の広さによっても明るさの感じ方は変わってくる。

実際に設置するまでは、期待した明るさになるか判断しにくいのが難しいところだ。

そこで便利なのが、明るさを調整できる調光機能である。設置してから明るすぎると感じても暗くできるし、くつろぐときには明るさを落とすこともできる。

光の色を変える調色機能と合わせて、調光・調色の両方に対応した製品を選ぶと、使い勝手がよくなる。

5. 平均演色評価数が高い製品は少ない

照明を選ぶうえで、もうひとつ注目したいのが「平均演色評価数」だ。

平均演色評価数は、一般にRaという記号で表される。基準となる光の下で見たときと比べて、照明の下で物の色がどの程度自然に見えるかを評価する指標だ。

一般的なLED照明には、青色LEDの光を黄色の蛍光体に当て、白色の光を作る方式が広く使われている。

この方式でも白色光を作れるが、光の波長ごとのバランスによっては、赤色などの色が自然に見えにくくなることがある。

演色性を重視した製品には、赤色LEDを追加するなどして光のスペクトルを調整し、色の再現性を高めたものもある。

ただし、赤色LEDを追加すれば必ず演色性が高くなるという単純な話ではない。実際の性能は製品ごとに異なるため、購入時にはRaの数値を確認するのが確実だ。

食事をおいしそうに見せたいなら演色性にも注目

演色性は、部屋の雰囲気だけでなく、食事の見え方にも関係する。

例えば、肉の赤みや野菜の色、ご飯の白さなどは、照明の光の性質によって見え方が変わる。演色性が低い照明では、食材本来の色がくすんで見えたり、不自然な色に見えたりすることがある。

食卓で使う照明なら、明るさや光の色だけでなく、演色性にもこだわりたい。

Ra80以上がひとつの目安になるが、より自然な色の見え方を求めるならRa90以上の製品も候補になる。

ただし、平均演色評価数だけですべての色の再現性を判断できるわけではない。赤色の再現性を特に重視する場合は、製品によってはR9などの特殊演色評価数も参考になる。

料理をおいしそうに見せたい人や、衣服・化粧品などの色を自然に確認したい人は、演色性を意識して照明を選ぶとよいだろう。

LED照明は調光・調色・演色性を確認して選ぼう

蛍光灯からLED照明へ交換するときは、単に消費電力や価格を比較するだけでは、使い始めてから不満が出ることがある。

今回、特に重要だと感じたのは次のポイントだ。

  • 操作方法: 紐で操作するタイプか、リモコンタイプか
  • 光の広がり方: 天井や壁まで明るくできるか
  • 調色機能: 昼白色や電球色などに切り替えられるか
  • 調光機能: 部屋に合わせて明るさを調整できるか
  • 演色性: Raの数値が十分に高いか

LED照明は、蛍光灯のようにランプを交換して光の色を変えるという使い方ができない製品も多い。そのため、最初の製品選びが重要になる。

特に、設置してみないとわかりにくい明るさについては、調光機能があると調整しやすい。また、食事や色の見え方を重視するなら、演色性も確認しておきたい。

価格の安さや明るさだけで決めず、自分がどのような部屋で、どのように照明を使いたいのかを考えて選ぶことが、LED照明への交換で失敗しないためのポイントだ。

元の文章

蛍光灯の販売が禁止されると言われる来年に向けて照明器具が壊れたときにLEDに変えていっている。部屋の構造で言うと紐で引っ張るタイプの方がどこにスイッチがあるかがわかりやすい。だけどペンダントライトのほうが値段が張る。なのでリモコンタイプのほうが安く種類が多い。

  • 照明で気になったのがまず青くギラつく
  • 天井部が暗くなる
  • リモコンタイプ出ないと色味が変更できない。
  • 明るさは設置しないとわからない
  • 平均演色評価数が高いものが少ない
  • 食事時の照明は演色性が高いほうが美味しく見える 色にこだわる人は演色性が高いものを選ぼう

蛍光灯タイプなら照明部の電球を変えると色味が変えれるが、LEDははじめから色味を変えるものを買わないといけない。明るさは設置しないとわからないので明るさも調整できたほうがいい。

平均演色評価数が高いものが少ないのは青色LEDに黄色の蛍光体を光らせるものが多く赤色が少ない。演色性にこだわったものは赤色LEDを別に付けているものがある。

10月1日から10月7日までキンドル新刊ピックアップ

10月1日発売の本

10月1日発売のそのほかの本 kyukyunyorituryo.github.io

10月2日発売の本

10月2日発売のそのほかの本 kyukyunyorituryo.github.io

10月3日発売の本

10月3日発売のそのほかの本 kyukyunyorituryo.github.io

10月4日発売の本

10月4日発売のそのほかの本 kyukyunyorituryo.github.io

10月5日発売の本

10月5日発売のそのほかの本 kyukyunyorituryo.github.io

10月6日発売の本

10月6日発売のそのほかの本 kyukyunyorituryo.github.io

10月7日発売の本

10月7日発売のそのほかの本 kyukyunyorituryo.github.io

9月24日から9月30日までキンドル新刊ピックアップ

9月24日発売の本

9月24日発売のそのほかの本 kyukyunyorituryo.github.io

9月25日発売の本

9月25日発売のそのほかの本 kyukyunyorituryo.github.io

9月26日発売の本

9月26日発売のそのほかの本 kyukyunyorituryo.github.io

9月27日発売の本

9月27日発売のそのほかの本 kyukyunyorituryo.github.io

9月28日発売の本

9月28日発売のそのほかの本 kyukyunyorituryo.github.io

9月29日発売の本

9月29日発売のそのほかの本 kyukyunyorituryo.github.io

9月30日発売の本

9月30日発売のそのほかの本 kyukyunyorituryo.github.io

jpackageのmsi作成処理をGitHub Actionsに追加するには

jpackageのmsi作成処理をgithub actionに追加するには

chatgpt:

できます。現在の構成を見ると、Linux の nightly build はそのまま残し、Windows 用に別 job を追加して jpackage --type msi を実行するのが一番自然です。

現在の nightly.yml は ubuntu-latest で JAR を作っているため、MSIだけは windows-latest で作る必要があります。GitHub Actions では Windows runner が利用できます。(GitHub Docs)

また、現在の build.gradle は Java 21 の jar タスクと create_run_jar を使っているので、まずは既存の create_run_jar の成果物をそのまま jpackage に渡す形が変更が少ないです。nightly.yml でも現在 gradle create_run_jar → build/libs/AozoraEpub3.jar を out/ にコピーしています。

まずおすすめする構成

今の

Ubuntu
  ↓
JAR作成
  ↓
artifact

に加えて、

Windows
  ↓
JAR作成
  ↓
jpackage
  ↓
AozoraEpub3-1.1.1.31.msi
  ↓
artifact

を追加します。

nightly.yml

例えばこうできます。

name: Nightly Build

on:
  - push
  - workflow_dispatch

jobs:
  build_artifacts:
    runs-on: ubuntu-latest

    steps:
      - uses: actions/checkout@v4

      - name: Get version from source code
        run: |
          >> $GITHUB_ENV echo PKG_VER=$(egrep 'VERSION\s+=' src/AozoraEpub3.java | awk -F '"' '{print $2}')

      - uses: actions/setup-java@v4
        with:
          java-version: '21'
          distribution: 'zulu'
          cache: 'gradle'

      - run: gradle build

      - run: mkdir -p bin

      - run: gradle create_run_jar

      - run: cp -v build/libs/AozoraEpub3.jar out/

      - run: cp -vr lib/LICENSE out/

      - uses: actions/upload-artifact@v4
        with:
          name: AozoraEpub3-${{ env.PKG_VER }}
          path: out/


  build_msi:
    runs-on: windows-latest

    steps:
      - uses: actions/checkout@v4

      - name: Get version from source code
        shell: bash
        run: |
          echo "PKG_VER=$(grep -E 'VERSION\s+=' src/AozoraEpub3.java | awk -F '"' '{print $2}')" >> "$GITHUB_ENV"

      - uses: actions/setup-java@v4
        with:
          java-version: '21'
          distribution: 'zulu'
          cache: 'gradle'

      - name: Build JAR
        shell: cmd
        run: gradlew.bat create_run_jar

      - name: Prepare jpackage input
        shell: pwsh
        run: |
          New-Item -ItemType Directory -Force -Path jpackage-input
          Copy-Item build/libs/AozoraEpub3.jar jpackage-input/
          Copy-Item AozoraEpub3.ico jpackage-input/
          Copy-Item -Recurse web jpackage-input/ -ErrorAction SilentlyContinue
          Copy-Item -Recurse profiles jpackage-input/ -ErrorAction SilentlyContinue

      - name: Create MSI
        shell: cmd
        run: >
          jpackage
          --type msi
          --win-menu
          --win-dir-chooser
          --win-shortcut
          --win-shortcut-prompt
          --copyright "kyukyunyorituryo"
          --icon AozoraEpub3.ico
          --app-version %PKG_VER%
          --description "改造版AozoraEpub3 青空文庫テキストePub3変換"
          -n "AozoraEpub3"
          --main-jar AozoraEpub3.jar
          --input jpackage-input
          --dest out

      - name: Upload MSI
        uses: actions/upload-artifact@v4
        with:
          name: AozoraEpub3-${{ env.PKG_VER }}-msi
          path: out/*.msi

ただし、ここには1点確認が必要です。

あなたの実際のリポジトリで、

web/
profiles/
AozoraEpub3.ico

がどこにあるかです。

現在の build.gradle では resources に src を指定しています。

resources {
    srcDirs = ["src"]
}

なので、web や profiles が src 以下に入っているなら、上の

Copy-Item -Recurse web jpackage-input/

は変える必要があります。


さらにおすすめ:build.gradle に jpackage タスクを作る

個人的には、今回はこちらの方をおすすめします。

現在、

ant.importBuild 'ant.xml'

もありますし、既存のビルド処理をGradleから呼び出しているので、jpackageもGradleタスクにしてしまうと管理しやすいです。

例えば、

tasks.register('jpackageMsi', Exec) {
    dependsOn 'create_run_jar'

    def inputDir = layout.buildDirectory.dir('jpackage-input').get().asFile
    def outputDir = layout.buildDirectory.dir('jpackage').get().asFile

    doFirst {
        inputDir.mkdirs()
        outputDir.mkdirs()

        copy {
            from 'build/libs/AozoraEpub3.jar'
            into inputDir
        }

        copy {
            from 'AozoraEpub3.ico'
            into inputDir
        }
    }

    commandLine 'jpackage',
            '--type', 'msi',
            '--win-menu',
            '--win-dir-chooser',
            '--win-shortcut',
            '--win-shortcut-prompt',
            '--copyright', 'kyukyunyorituryo',
            '--icon', 'AozoraEpub3.ico',
            '--app-version', '1.1.1.31',
            '--description', '改造版AozoraEpub3 青空文庫テキストePub3変換',
            '-n', 'AozoraEpub3',
            '--main-jar', 'AozoraEpub3.jar',
            '--input', inputDir.absolutePath,
            '--dest', outputDir.absolutePath
}

そしてGitHub Actionsは、

- name: Create MSI
  run: gradlew.bat jpackageMsi

だけにできます。


ただしバージョンは固定しない方がいい

現在のコマンドでは、

--app-version 1.1.1.31

と固定されています。

一方、nightly.yml はソースコードから

PKG_VER

を取得しています。

なのでGradle側でもバージョンをソースから取得するか、GitHub Actionsから渡す方がいいです。

例えば、

- name: Create MSI
  run: gradlew.bat jpackageMsi -PpkgVersion=${{ env.PKG_VER }}

として、

def pkgVersion = project.findProperty('pkgVersion') ?: '1.1.1.31'

とします。

そして、

'--app-version', pkgVersion,

とします。

これなら、

src/AozoraEpub3.java
        ↓
VERSION
        ↓
PKG_VER
        ↓
jpackage --app-version
        ↓
AozoraEpub3-1.1.1.31.msi

と一元化できます。


WiXについて

jpackage --type msi はWindows側でMSIを生成するためのWiX環境が必要です。

現在のGitHub-hosted windows-2025 イメージには WiX Toolset 3.14.1 が既に入っています。(GitHub)

そのため、まずは

runs-on: windows-latest

で試すのが簡単です。

jpackage 自体はJDK 21に含まれているので、既存の

actions/setup-java@v4
java-version: '21'

で使えます。


なお、あなたの現在の構成なら「別ジョブ」がいい

既存の

build_artifacts:
  runs-on: ubuntu-latest

をWindowsに変更するのではなく、

build_artifacts
    └─ Ubuntu
       └─ JAR

build_msi
    └─ Windows
       └─ JAR
       └─ jpackage
       └─ MSI

とするのがいいです。

理由は、jpackage は対象OS上でパッケージを作るという前提だからです。Linux runnerでWindows MSIを作る構成にするより、Windows runnerで直接作った方が単純です。

また、最近のGitHub ActionsのWindows runnerにはWiXが含まれているため、まずは追加インストールなしで試せます。(GitHub)

次にやるなら、実際の build.gradle の create_run_jar と ant.xml の構成に合わせて、jpackageMsi タスクを最小変更で追加するのがよいです。 今回アップロードされた build.gradle では ant.xml 側の処理が見えていないため、そこだけ確認できれば、かなり正確に組めます。

jpackage で作ったMSIをインストールして起動したときの Failed to launch JVM なら、まず疑うべきなのは アプリ本体ではなく、jpackageが同梱したJRE/JVMの起動設定です。

今回の構成だと、特に --main-jar と --input の中身を確認したいです。

まず確認したいこと

MSIをインストールした後、インストール先に通常、

AozoraEpub3/
├─ app/
│  ├─ AozoraEpub3.jar
│  └─ ...
├─ runtime/
│  └─ bin/
│     └─ java.exe
└─ AozoraEpub3.exe

のような構成ができます。

そこで インストール先の app フォルダに何が入っているか確認してください。

特に今回のjpackageコマンドが

--main-jar AozoraEpub3.jar
--input C:\Users\Owner\Desktop\AozoraEpub3-1.1.1b31Q

なら、入力ディレクトリには最低限、

AozoraEpub3-1.1.1b31Q/
└─ AozoraEpub3.jar

が必要です。


一番重要なのは、コマンドラインからJARが起動するか

MSIを作る前に、同じJARについて、

java -jar AozoraEpub3.jar

が正常に動くか確認してください。

これが動くのにMSIだけ

Failed to launch JVM

になるなら、jpackage 側の問題にかなり絞れます。

逆に、

java -jar AozoraEpub3.jar

でも起動しないなら、JAR側の問題です。


また、jpackageで直接試す

MSIではなく app-image を作ると原因を切り分けやすいです。

jpackage ^
  --type app-image ^
  --win-menu ^
  --win-dir-chooser ^
  --win-shortcut ^
  --win-shortcut-prompt ^
  --copyright "kyukyunyorituryo" ^
  --icon AozoraEpub3.ico ^
  --app-version 1.1.1.31 ^
  --description "改造版AozoraEpub3 青空文庫テキストePub3変換" ^
  -n "AozoraEpub3" ^
  --main-jar AozoraEpub3.jar ^
  --input C:\Users\Owner\Desktop\AozoraEpub3-1.1.1b31Q

生成された、

AozoraEpub3\
    AozoraEpub3.exe

を直接起動します。

これでも

Failed to launch JVM

なら、MSIではなく jpackageのアプリイメージ生成段階の設定を調べるべきです。


今回かなり気になる点

以前のコードで、

System.getProperty("jpackage.app-path")

を使ってjpackage環境を判定していましたが、これは JVMが正常に起動した後のJavaコードなので、今回の

Failed to launch JVM

より前の段階で失敗しているなら、これは原因ではありません。

つまり、

EXE
 ↓
JVM起動 ← ★ここで失敗
 ↓
AozoraEpub3.jar
 ↓
main()
 ↓
jpackage.app-path判定

なので、まずJVM起動自体を確認する必要があります。


もし今回の Failed to launch JVM が GitHub ActionsでMSIを作った後にインストールしたWindowsで発生しているのであれば、さらに jpackage の実行ログと、生成された app フォルダの内容を見るのが早いです。

jpackage を実行したときのコマンド全文と、起動時に表示される Failed to launch JVM の画面(またはログ)を貼ってもらえれば、原因をかなり絞れます。 なるほど。「Failed to launch JVM」の原因として、ant.xmlで行っていたファイルコピーがjpackage側では再現されていない可能性が高いです。

jpackageは基本的に、

--input
    AozoraEpub3.jar

のように指定したファイルしかアプリイメージへ入れません。 ant.xmlで例えば、

web/
profiles/
*.properties
*.ini
images/
lib/

などをコピーしていたなら、そのコピー処理をjpackage用のinputディレクトリ作成時に再現する必要があります。

今回の構成なら、MSI作成前に例えば、

build/jpackage-input/
├─ AozoraEpub3.jar
├─ web/
├─ profiles/
├─ images/
└─ その他必要ファイル

を作ってから、

jpackage --input build/jpackage-input ...

とするのがよいです。

特に重要なのは、ant.xmlのコピー処理をそのままGradleまたはGitHub Actionsへ移植することです。

もし ant.xml をこちらに渡してもらえれば、そこに書かれている <copy> や <fileset> を確認して、現在のGradle/GitHub Actionsに最小限の変更で組み込む形にできます。 はい。ant.xmlを見ると、原因がかなりはっきりしました。

create_run_jar は単に AozoraEpub3.jar を作っているだけではなく、実行に必要な大量の外部ファイルを out/ にコピーしています。例えば gaiji、presets、template、web、各種 chuki_*.txt などです。

特に重要なのはこの部分です。

<copy file="${dir.buildfile}/gaiji/convert.pe" todir="${dir.jarfile}/gaiji"/>
...
<copy file="${dir.buildfile}/presets/kindle_fire.ini" todir="${dir.jarfile}/presets"/>
...
<copy file="${dir.buildfile}/template/mimetype" todir="${dir.jarfile}/template/"/>
...
<copy file="${dir.buildfile}/web/extract_sample.txt" todir="${dir.jarfile}/web/"/>

そして最後に、

<copy file="${dir.buildfile}/AozoraEpub3.ico" todir="${dir.jarfile}"/>
<copy file="${dir.buildfile}/chuki_alt.txt" todir="${dir.jarfile}"/>
...
<copy file="${dir.buildfile}/README_Changes.txt" todir="${dir.jarfile}"/>

までコピーしています。

つまり、先ほどのMSI作成方法で

AozoraEpub3.jar

だけを jpackage --input に渡していたなら、従来のAntビルドと全く同じ実行環境になっていません。

一番簡単な修正

むしろ既存の ant.xml を利用するのがよいです。

現在、

gradle create_run_jar

を実行しているなら、Windows側でも

gradlew.bat create_run_jar

として、その結果の out/ をそのまま jpackage の --input に渡します。

つまり、

プロジェクト
 ├─ out/
 │   ├─ AozoraEpub3.jar
 │   ├─ gaiji/
 │   ├─ presets/
 │   ├─ template/
 │   ├─ web/
 │   ├─ chuki_alt.txt
 │   ├─ chuki_ivs.txt
 │   ├─ ...
 │   └─ AozoraEpub3.ico

という従来の構成を作ってから、

jpackage ^
  --type msi ^
  --input out ^
  --main-jar AozoraEpub3.jar ^
  ...

とするのが最小変更です。

ただし、jpackageでは --input の中身がそのままアプリの app ディレクトリへコピーされるので、これは今回の目的にも非常に合っています。

ただし一つ注意

Antの処理では out/lib に依存JARをコピーする部分はコメントアウトされています。

そのため、現在の AozoraEpub3.jar が依存ライブラリをどう扱っているかは、build.gradle 側の create_run_jar 実装も確認した方がいいです。

結論としては、MSI用に別途大量の <copy> を書き直す必要はありません。

gradle create_run_jar
        ↓
      out/
        ↓
jpackage --input out
        ↓
      MSI

という流れにするのが、既存のAntビルドを最も忠実に利用できる方法です。 そういう構成なら、原因がかなり明確です。

もともとの ant.xml は「JARを作る」だけでなく、JARの外側に必要な実行時ファイル一式を out/ に配置する役割も持っていたわけですね。

一方、現在は、

ant.xml
  ↓
AozoraEpub3.jar
  +
gaiji/
presets/
template/
web/
chuki_*.txt
...

という構成から、

Gradle
  ↓
fat JAR
  ↓
AozoraEpub3.jar

へ変更した。

そのため、fat JAR化したことによってJavaライブラリはJAR内に入ったとしても、ant.xmlがコピーしていた「アプリ固有の外部ファイル」は自動的には移行されません。

つまりMSIの問題はここ

jpackageに渡しているものが、

AozoraEpub3.jar

だけだと、

gaiji/
presets/
template/
web/
chuki_alt.txt
chuki_ivs.txt
chuki_latin.txt
chuki_tag.txt
chuki_tag_suf.txt
chuki_utf.txt

などが存在しません。

ant.xmlではこれらを明示的にコピーしていたので、従来の実行環境には存在していました。

したがって、fat JAR化した現在のGradle構成でも、この「アプリ固有ファイルの配置」は別途必要です。


一番変更が少ない方法

Gradleでfat JARを作る処理に、

out/
 ├─ AozoraEpub3.jar
 ├─ gaiji/
 ├─ presets/
 ├─ template/
 ├─ web/
 ├─ chuki_alt.txt
 ├─ chuki_ivs.txt
 ├─ chuki_latin.txt
 ├─ chuki_tag.txt
 ├─ chuki_tag_suf.txt
 └─ chuki_utf.txt

を作る処理を追加します。

そしてjpackageは、

jpackage ^
  --type msi ^
  --input out ^
  --main-jar AozoraEpub3.jar ^
  ...

とします。

つまり、

Gradle fat JAR
      ↓
      ├── AozoraEpub3.jar
      ├── gaiji/
      ├── presets/
      ├── template/
      ├── web/
      └── chuki_*.txt
             ↓
          jpackage
             ↓
          MSI

です。

重要なのは

ant.xmlをもう実行する必要はありません。

ant.xmlは「昔のJAR作成+必要ファイルコピー」の仕組みなので、現在は

JAR作成 → Gradle
必要ファイルコピー → Gradle
MSI作成 → jpackage

と役割を分けるのが自然です。

また、fat JARにしたことで lib/*.jar を外に置く必要もなくなっているはずなので、ant.xmlの依存ライブラリコピー部分をそのまま再現する必要もありません。

次に確認したいのは、現在の build.gradle の create_run_jar 周辺です。 そこに最小限の copy を追加すれば、ant.xmlのコピー処理をGradleへ移せます。 できます。むしろ今回の目的なら、ant.xmlをそのまま利用して、Gradleでfat JARを作った後にant.xmlの「ファイルコピー処理」だけを実行する方法が一番変更が少ないです。

ただし、ant.xml の create_run_jar はJAR自体も作るので、そのターゲットをそのまま呼ぶと、Gradleで作ったfat JARを上書きしてしまいます。

したがって、ant.xml にあるコピー処理を実行する専用ターゲットを追加するのが安全です。

例えば、

<target name="copy_runtime_files">
    <copy todir="${dir.jarfile}/gaiji">
        ...
    </copy>

    <copy todir="${dir.jarfile}/presets">
        ...
    </copy>

    <copy todir="${dir.jarfile}/template">
        ...
    </copy>

    <copy todir="${dir.jarfile}/web">
        ...
    </copy>

    <copy file="..." todir="${dir.jarfile}"/>
    ...
</target>

のように、既存の create_run_jar に入っている <copy> をそのまま移す形です。

そしてGradleからAntを呼びます。

tasks.register("copyRuntimeFiles") {
    doLast {
        ant.antfile("ant.xml", target: "copy_runtime_files")
    }
}

そのうえで、

gradle build
      ↓
fat JAR作成
      ↓
gradle copyRuntimeFiles
      ↓
out/
 ├─ AozoraEpub3.jar       ← Gradleのfat JAR
 ├─ gaiji/
 ├─ presets/
 ├─ template/
 ├─ web/
 ├─ chuki_alt.txt
 ├─ chuki_ivs.txt
 ├─ chuki_latin.txt
 ├─ chuki_tag.txt
 ├─ chuki_tag_suf.txt
 └─ chuki_utf.txt
      ↓
jpackage --input out

という流れにできます。

さらに変更を少なくするなら

ant.xml の既存の create_run_jar を、

<target name="create_run_jar" depends="copy_runtime_files">
    ...
</target>

のように変更する方法もありますが、今回はGradle側ですでにfat JARを作っているので、create_run_jar は従来の用途として残して、MSI用には copy_runtime_files を追加するほうが分かりやすいです。

特に今回の ant.xml は、gaiji、presets、template、web、各種 chuki_*.txt などのコピー処理がすでに完成しているので、そのコピー内容をGradleへ書き直す必要はありません。既存のAnt処理をそのまま再利用できます。

もし現在の build.gradle を基準にするなら、create_run_jar をどのように変更すれば「fat JAR → ant.xmlのコピー → jpackage」になるか、実際のGradleコードに合わせて最小変更で書けます。 はい。今回の構成なら、ant.xmlのJAR作成処理を削除して、コピー処理だけ残すのが一番簡単だと思います。

現在は、

Gradle
 └─ fat JARを作成
       ↓
ant.xml
 └─ 必要ファイルをout/へコピー
       ↓
jpackage
 └─ out/をMSI化

にすればよいです。

ant.xmlをこうする

現在の

<target name="create_run_jar">
    <jar destfile="${dir.jarfile}/AozoraEpub3.jar">
        ...
    </jar>

    <copy ... />
    <copy ... />
    ...
</target>

から、<jar> 部分だけ削除して、

<target name="copy_runtime_files">
    <copy ... />
    <copy ... />
    ...
</target>

とします。

つまり、既存の <copy> 処理は一切変更しない。

ant.xml にある out/AozoraEpub3.jar の生成部分は、現在のGradleのfat JAR生成と役割が重複しています。元のAnt処理では bin/ をJARに入れていましたが、現在はGradleがfat JARを作るため、ここを残す意味がありません。

Gradleから呼ぶ

例えば、

tasks.register("copyRuntimeFiles") {
    dependsOn("jar")

    doLast {
        ant.antfile("ant.xml", target: "copy_runtime_files")
    }
}

そしてMSI作成前に、

gradle copyRuntimeFiles

とします。

ただし、現在のfat JARの出力先が build/libs/AozoraEpub3.jar なら、Antの out/ にコピーする処理だけ1行追加すればよいです。

copy {
    from jar
    into "out"
}

最終的には、

build/libs/AozoraEpub3.jar
        ↓
      out/
        ├── AozoraEpub3.jar
        ├── gaiji/
        ├── presets/
        ├── template/
        ├── web/
        └── chuki_*.txt
        ↓
     jpackage
        ↓
      MSI

となります。

なので、今回なら「AntをGradleに移植する」より、「AntからJAR生成部分だけ捨てて、既存のコピー処理をそのまま再利用する」のが最小変更です。 はい。Ant側はJAR作成をやめてコピー処理だけにし、Gradleのfat JARを out/ に置いてから、Antのコピー処理を実行して、その out/ をそのままjpackageに渡す形なら、かなりシンプルです。

完成形は以下です。

tasks.register('jpackageMsi', Exec) {
    dependsOn 'create_run_jar'

    def inputDir = file('out')
    def outputDir = layout.buildDirectory.dir('jpackage').get().asFile

    doFirst {
        inputDir.mkdirs()
        outputDir.mkdirs()

        // Gradleで作成したfat JARをoutへ配置
        copy {
            from 'build/libs/AozoraEpub3.jar'
            into inputDir
        }

        // ant.xmlの実行時ファイルコピー処理
        ant.antfile('ant.xml', target: 'copy_runtime_files')
    }

    commandLine 'jpackage',
            '--type', 'msi',
            '--win-menu',
            '--win-dir-chooser',
            '--win-shortcut',
            '--win-shortcut-prompt',
            '--copyright', 'kyukyunyorituryo',
            '--icon', 'AozoraEpub3.ico',
            '--app-version', '1.1.1.33',
            '--description', '改造版AozoraEpub3 青空文庫テキストePub3変換',
            '-n', 'AozoraEpub3',
            '--main-jar', 'AozoraEpub3.jar',
            '--input', inputDir.absolutePath,
            '--dest', outputDir.absolutePath
}

そして ant.xml は、JAR生成部分を削除して、

<target name="copy_runtime_files">

    <!-- ここに元のcreate_run_jarにあったcopy処理をそのまま残す -->

</target>

とします。

ただし、1点だけ重要です。

元の ant.xml のコピー先は、

${dir.jarfile}

で、元ファイルでは

<property name="dir.jarfile" location="./out"/>

となっています。

そのため、上記のGradleでは、

out/

をjpackageの入力ディレクトリにしています。

最終的に、

out/
├── AozoraEpub3.jar       ← Gradleのfat JAR
├── gaiji/
├── presets/
├── template/
├── web/
├── chuki_alt.txt
├── chuki_ivs.txt
├── chuki_latin.txt
├── chuki_tag.txt
├── chuki_tag_suf.txt
├── chuki_utf.txt
├── LICENSE.txt
└── ...

となります。

元のAntがコピーしていた gaiji、presets、template、web、各種 chuki_*.txt などもそのまま残せます。

さらに1つ直した方がいいところ

現在の

copy {
    from 'AozoraEpub3.ico'
    into inputDir
}

は不要です。

jpackageの

'--icon', 'AozoraEpub3.ico',

はjpackageを実行する側から見たパスなので、inputDir にコピーする必要はありません。

したがって、完成形はこれで十分です。

tasks.register('jpackageMsi', Exec) {
    dependsOn 'create_run_jar'

    def inputDir = file('out')
    def outputDir = layout.buildDirectory.dir('jpackage').get().asFile

    doFirst {
        inputDir.mkdirs()
        outputDir.mkdirs()

        copy {
            from 'build/libs/AozoraEpub3.jar'
            into inputDir
        }

        ant.antfile('ant.xml', target: 'copy_runtime_files')
    }

    commandLine 'jpackage',
            '--type', 'msi',
            '--win-menu',
            '--win-dir-chooser',
            '--win-shortcut',
            '--win-shortcut-prompt',
            '--copyright', 'kyukyunyorituryo',
            '--icon', 'AozoraEpub3.ico',
            '--app-version', '1.1.1.33',
            '--description', '改造版AozoraEpub3 青空文庫テキストePub3変換',
            '-n', 'AozoraEpub3',
            '--main-jar', 'AozoraEpub3.jar',
            '--input', inputDir.absolutePath,
            '--dest', outputDir.absolutePath
}

これならfat JARの作成はGradle、既存のリソース配置はant.xml、MSI化はjpackageという役割分担になり、既存のAntのコピー処理をほぼそのまま利用できます。 はい。nightly.yml も create_run_jar から変更した方がよいです。

現在は役割が変わっているので、

以前
create_run_jar
  └─ AntでJAR作成+必要ファイルコピー

現在
Gradle
  ├─ fat JAR作成
  └─ Antで必要ファイルコピー

に合わせるべきです。

ただし、create_run_jar 自体を全部なくす必要はありません。既存のGradleタスクがfat JARを作るタスクなら、それを直接使うのがよいです。

たとえば現在のnightly.ymlが、

- run: gradle build
- run: mkdir -p bin
- run: gradle create_run_jar
- run: cp -v build/libs/AozoraEpub3.jar out/
- run: cp -vr lib/LICENSE out/

なら、基本的には、

- run: gradle build
- run: mkdir -p out
- run: gradle copy_runtime_files
- run: cp -v build/libs/AozoraEpub3.jar out/

という流れにします。

ただし順番は、fat JARを out/ にコピーしてから copy_runtime_files を実行しても問題ありません。

おすすめは、

- run: gradle build
- run: mkdir -p out
- run: cp -v build/libs/AozoraEpub3.jar out/
- run: gradle copy_runtime_files

です。

すると、

gradle build
    ↓
build/libs/AozoraEpub3.jar
    ↓
out/AozoraEpub3.jar
    ↓
gradle copy_runtime_files
    ↓
out/
 ├─ AozoraEpub3.jar
 ├─ gaiji/
 ├─ presets/
 ├─ template/
 ├─ web/
 ├─ chuki_*.txt
 └─ ...

となります。

そしてMSI側も同じ out/ を使えば、

nightly.yml
    ↓
Gradle fat JAR
    ↓
out/ ← Antの既存コピー処理
    ↓
jpackage --input out

で統一できます。

なので、nightly.yml の create_run_jar は copy_runtime_files に変更する方向でよいです。

ただし、今の build.gradle にある create_run_jar が実際には何をしているかによって、gradle build だけでfat JARが完成するのか、別タスクが必要なのかが変わります。そこだけ確認してから最終形を決めるのが安全です。

Java swingでjarがあるフォルダーに設定を保存していたのをAppDataフォルダに保存と切り替えるには

java swingでjarがあるフォルダーに設定を保存していたのをAppDataフォルダに保存と切り替えるには

gemini:

JavaのSwingアプリケーションで、設定ファイルの保存先をJARがあるフォルダからWindowsの AppDataフォルダ(%APPDATA%) に切り替えるには、環境変数 APPDATA を取得し、そこにアプリケーション固有のフォルダを作成して各種パスを再設定します。 提示していただいたコードをベースに、AppData内に「AozoraEpub3」などのアプリ用フォルダを作り、そこへ各種パス(設定、キャッシュ、プロファイルなど)を紐付ける修正例を作成しました。

💡 修正のポイント

  1. System.getenv("APPDATA") を使って、Windowsの C:\Users\ユーザー名\AppData\Roaming のパスを取得します(MacやLinuxなど他OSのことも考慮し、取得できない場合のフォールバックとしてホームフォルダ user.home も指定しておくと親切です)。
  2. AppData直下にファイルを散らかさないよう、AozoraEpub3 というサブフォルダを作成します。
  3. これまで this.jarPath を基準にしていたパス(cachePath, profilePath などの生成や、Properties の読み込み)を、新しく定義した this.appDataPath を基準にするよう書き換えます。

🛠️ 修正後のコード例

 AozoraEpub3Applet(String title) throws MalformedURLException, URISyntaxException
{
    setTitle(title);
    //アイコン設定
    setIconImage(new ImageIcon(Objects.requireNonNull(AozoraEpub3Applet.class.getResource("images/icon.png"))).getImage());
    //最小サイズ
    setMinimumSize(new Dimension(540, 320));
    setPreferredSize(new Dimension(540, 400));

    setSize(new Dimension(520, 460));

    JMenuBar menubar = new JMenuBar();

    JMenu menu1 = new JMenu("ヘルプ");

    menubar.add(menu1);

    JMenuItem menuitem1 = new JMenuItem("オンラインヘルプ");
    JMenuItem menuitem2 = new JMenuItem("更新の確認");
    JMenuItem menuitem3 = new JMenuItem("設定を保存せずに閉じる");
    JMenuItem menuitem4 = new JMenuItem("作者のページ");

    menu1.add(menuitem1);
    menu1.add(menuitem2);
    menu1.add(menuitem3);
    menu1.add(menuitem4);


    menuitem1.addActionListener(e -> {
        Desktop desktop = Desktop.getDesktop();
        try {
            desktop.browse(new URI("https://github.com/kyukyunyorituryo/AozoraEpub3/wiki"));
        } catch (IOException | URISyntaxException exx) {
            exx.printStackTrace();
        }
    });
    menuitem2.addActionListener(e -> {
        Desktop desktop = Desktop.getDesktop();
        try {
            desktop.browse(new URI("https://github.com/kyukyunyorituryo/AozoraEpub3/releases"));
        } catch (IOException | URISyntaxException exx) {
            exx.printStackTrace();
        }
    });
    menuitem3.addActionListener(e -> System.exit(0));
    menuitem4.addActionListener(e -> {
        Desktop desktop = Desktop.getDesktop();
        try {
            desktop.browse(new URI("https://kyukyunyorituryo.github.io/"));
        } catch (IOException | URISyntaxException exx) {
            exx.printStackTrace();
        }
    });
    setJMenuBar(menubar);

    // --- パス関連初期化(AppDataへの切り替え) ---
    this.jarPath = ""; // 既存の構成を維持

    // 1. WindowsのAppData(Roaming)のパスを取得。他OSの場合はユーザーホームにフォールバック
    String appData = System.getenv("APPDATA");
    File baseDir;
    if (appData != null) {
        // Windows: C:\Users\xxx\AppData\Roaming\AozoraEpub3
        baseDir = new File(appData, "AozoraEpub3");
    } else {
        // Mac/Linux等: /Users/xxx/.AozoraEpub3
        baseDir = new File(System.getProperty("user.home"), ".AozoraEpub3");
    }

    // 2. アプリ用フォルダがなければ作成する
    if (!baseDir.exists()) {
        baseDir.mkdirs();
    }

    // 3. 各種パスをAppData内のフォルダ配下に変更
    this.configPath = determineConfigPath(propFileName); // 必要に応じて引数や内部処理を調整してください

    this.cachePath = new File(baseDir, ".cache");
    this.webConfigPath = new File(baseDir, "web");
    this.profilePath = new File(baseDir, "profiles");

    // フォルダの作成を確実に実施
    if (!this.profilePath.exists()) this.profilePath.mkdir();
    if (!this.cachePath.exists()) this.cachePath.mkdir();
    if (!this.webConfigPath.exists()) this.webConfigPath.mkdir();

    // 設定ファイルのフルパスを定義
    File propFile = new File(baseDir, this.propFileName);

    // 設定ファイル読み込み
    props = new Properties();
    try {
        // Files.exists や FileInputStream も新パスを参照するように変更
        if (Files.exists(propFile.toPath())) {
            try (FileInputStream fos = new FileInputStream(propFile)) { // try-with-resources で安全にクローズ
                props.load(fos);
            }
        }
    } catch (Exception e) {
        throw new RuntimeException(e);
    }
    String path = props.getProperty("LastDir");
    if (path != null && !path.isEmpty()) this.currentPath = new File(path);
}

⚠️ 注意点・アドバイス

  • 設定の保存(書き込み)処理の修正もお忘れなく: 今回は初期化(読み込み)部分のコードでしたが、アプリ終了時などに Properties をファイルに保存(props.store() など)している箇所があるはずです。そこでも上記で設定した propFile(AppData内のパス)に対して書き込むように変更してください。
  • 移行処理(オプション): もし、すでにJARと同じフォルダに設定ファイル(this.propFileName)が存在している既存ユーザーがいる場合、自動でAppData側にファイルを移動(コピー)させるコードを最初に入れておくと、ユーザーの設定がリセットされずにスムーズに移行できます。

設定の保存(書き込み)処理など、他にも書き換えが必要な箇所で迷う部分があれば教えてください!引き続きサポートします。

jar配布時は以前のままでexe配布時にだけAppDataに保存するようにしたい

gemini

配布形態(JARかEXEか)によって保存先を自動で切り替えるには、Javaのシステムプロパティや環境変数、または実行ファイルのパス情報を判定に使用するのがスマートです。 特に、提示されたソースコードの元のリポジトリ(改造版AozoraEpub3)のリリースノートを確認すると、v1.1.1b28Q以降で AozoraEpub3-1.1.1.28.msi などのjpackage(またはLaunch4j等)によるWindowsインストーラー版(EXE化)が追加されています。 jpackage等でEXE化されて "C:\Program Files" などにインストールされた場合、アプリ自身がプログラムフォルダ内に設定を書き込もうとすると権限エラー(書き込み不可)になるため、AppDataへの切り替えが必須になります。

以下に、「EXE(インストーラー)実行時はAppData、JAR(ポータブル)実行時はJARと同じフォルダ」を自動判別して切り替える修正コードを提案します。

💡 切り替え判別のロジック

Windowsのインストーラー(jpackageなど)でEXE化されたアプリは、システムプロパティ jpackage.app-path を持っていたり、実行パスに Program Files や AppData\Local が含まれたりします。 最も確実でシンプルな判別方法は、「アプリの実行場所(JAR/EXEがあるフォルダ)が書き込み可能かどうか」、または 「特定のシステムプロパティが存在するか」 をチェックすることです。

今回は、最も堅牢な「実行フォルダが書き込み禁止(Program Filesなど)ならAppData、書き込み可能ならその場(JARと同階層)に保存する」というポータブル性も考慮したロジックで書き換えます。

🛠️ 修正後のコード例

詳細なコンストラクタおよびパス初期化の実装例は、以下のようになります。jpackage.app-path の有無やフォルダの書き込み権限を検知して baseDir や各種パス(cachePath, profilePath など)を AppData またはカレントディレクトリへ切り替えます。

AozoraEpub3Applet(String title) throws MalformedURLException, URISyntaxException {
    setTitle(title);
    setIconImage(new ImageIcon(Objects.requireNonNull(AozoraEpub3Applet.class.getResource("images/icon.png"))).getImage());
    setMinimumSize(new Dimension(540, 320));
    setPreferredSize(new Dimension(540, 400));
    setSize(new Dimension(520, 460));

    JMenuBar menubar = new JMenuBar();
    JMenu menu1 = new JMenu("ヘルプ");
    menubar.add(menu1);

    JMenuItem menuitem1 = new JMenuItem("オンラインヘルプ");
    JMenuItem menuitem2 = new JMenuItem("更新の確認");
    JMenuItem menuitem3 = new JMenuItem("設定を保存せずに閉じる");
    JMenuItem menuitem4 = new JMenuItem("作者のページ");

    menu1.add(menuitem1);
    menu1.add(menuitem2);
    menu1.add(menuitem3);
    menu1.add(menuitem4);

    menuitem1.addActionListener(e -> {
        Desktop desktop = Desktop.getDesktop();
        try { desktop.browse(new URI("https://github.com")); } catch (IOException | URISyntaxException exx) { exx.printStackTrace(); }
    });
    menuitem2.addActionListener(e -> {
        Desktop desktop = Desktop.getDesktop();
        try { desktop.browse(new URI("https://github.com")); } catch (IOException | URISyntaxException exx) { exx.printStackTrace(); }
    });
    menuitem3.addActionListener(e -> System.exit(0));
    menuitem4.addActionListener(e -> {
        Desktop desktop = Desktop.getDesktop();
        try { desktop.browse(new URI("https://github.io")); } catch (IOException | URISyntaxException exx) { exx.printStackTrace(); }
    });
    setJMenuBar(menubar);

    this.jarPath = ""; 
    File baseDir = new File(this.jarPath); 
    boolean isExeDeployment = System.getProperty("jpackage.app-path") != null;

    if (isExeDeployment || !Files.isWritable(Path.of("."))) {
        String appData = System.getenv("APPDATA");
        if (appData != null) {
            baseDir = new File(appData, "AozoraEpub3");
        } else {
            baseDir = new File(System.getProperty("user.home"), ".AozoraEpub3");
        }
        if (!baseDir.exists()) {
            baseDir.mkdirs();
        }
        this.jarPath = baseDir.getAbsolutePath() + File.separator;
    } else {
        this.jarPath = ""; 
        baseDir = new File(".");
    }

    this.configPath = determineConfigPath(propFileName);
    this.cachePath = new File(baseDir, ".cache");
    this.webConfigPath = new File(baseDir, "web");
    this.profilePath = new File(baseDir, "profiles");

    if (!this.profilePath.exists()) this.profilePath.mkdir();
    if (!this.cachePath.exists()) this.cachePath.mkdir();
    if (!this.webConfigPath.exists()) this.webConfigPath.mkdir();

    File propFile = new File(baseDir, this.propFileName);
    props = new Properties();
    try {
        if (Files.exists(propFile.toPath())) {
            try (FileInputStream fos = new FileInputStream(propFile)) {
                props.load(fos);
            }
        }
    } catch (Exception e) {
        throw new RuntimeException(e);
    }
    String path = props.getProperty("LastDir");
    if (path != null && !path.isEmpty()) this.currentPath = new File(path);
}

✨ この修正のメリット

  1. コードの一本化: JAR用とEXE用でソースコードを分ける必要がなく、環境に応じて動的に保存先を切り替えます。
  2. ポータブル性の維持: ZIP版などで書き込み可能な場所に展開した場合は、これまで通り同階層に設定等が保存されます。
  3. UAC権限エラーの回避: C:\Program Files 等にインストールされた場合でも、安全に AppData へ保存先を移行できます。

もし特定のパッケージングツール(Launch4jなど)を使用している場合は、判定条件の追加・調整が可能です。詳細なコード全体は参照元のリポジトリおよびドキュメントをご確認ください。

jpackageでexeを作るとthis.jarPath = "app/";にしないとファイルが読み込めない

gemini

jpackage を使って Windows 向けの EXE(または MSI)を作成した場合、アプリの内部構造が変わるためにその現象が発生します。 [1]

❓ なぜ app/ にする必要があるのか

jpackage でビルドされた Windows アプリのインストール先(C:\Program Files\アプリ名\ など)を覗くと、以下のような構造になっています。 [2, 3]

  • アプリ名.exe (直下にある起動エントリ)
  • runtime/ (内蔵された Java 実行環境)
  • app/ (メインの AozoraEpub3.jar や、同梱されたテンプレートなどのリソースファイルが配置されるフォルダ)

jpackage が生成した EXE をダブルクリックして起動したとき、プログラムの カレントディレクトリ(.)は app/ ではなく「EXE のあるルート階層」 になります。しかし、実際の JAR ファイルや内蔵されている設定用ファイル群は app/ の中に隔離されているため、これまでのポータブル版(JAR版)と同じ感覚で .(直下)を探しに行くと、「ファイルが見つからない」 となってしまうのが原因です。 [4, 5]

💡 解決策

「JAR配布時(ポータブル)」と「jpackageでのEXE配布時」でコードを書き換えずに自動でパスを正しく解決するには、先述した jpackage.app-path プロパティの有無を利用して、ベースとなるディレクトリ(baseDir)の切り替えロジックを以下のように組み替えるのが最も安全です。

1. 読み込み専用のファイル(template、web、profilesの初期データ等)

これらは jpackage によってパッケージングされ、app/ フォルダの中に固定されています。

2. 書き込みが発生するファイル(.cache、ユーザー設定の properties 等)

C:\Program Files の中は一般権限では書き込み禁止のため、EXE 実行時のみ AppData を見に行くようにします。 [2, 6]

🛠️ 自動判別を取り入れた修正コード例

この構造変化に対応しつつ、JAR版(従来通り)とEXE版(AppData・app/分離)を両立させる初期化コードの構成例です。

AozoraEpub3Applet(String title) throws MalformedURLException, URISyntaxException {
    // (メニューバーや各種ボタンの初期化部分は省略)
    setJMenuBar(menubar);

    // --- パス自動判定ロジック ---

    // jpackage で起動されているかチェック
    boolean isJPackageEnv = System.getProperty("jpackage.app-path") != null;

    File readBaseDir;  // テンプレートなどの読み込み専用ファイル用ベースパス
    File writeBaseDir; // 設定やキャッシュなどの書き込み用ベースパス

    if (isJPackageEnv) {
        // 【EXE配布時の挙動】
        // 1. 読み込み用: JARが配置されている "app" フォルダを指すようにする
        readBaseDir = new File("app");
        this.jarPath = "app" + File.separator; // template読み込みエラーの回避用

        // 2. 書き込み用: 権限のある Windows の AppData を指定
        String appData = System.getenv("APPDATA");
        if (appData != null) {
            writeBaseDir = new File(appData, "AozoraEpub3");
        } else {
            writeBaseDir = new File(System.getProperty("user.home"), ".AozoraEpub3");
        }
        if (!writeBaseDir.exists()) {
            writeBaseDir.mkdirs();
        }
    } else {
        // 【従来のJAR配布時の挙動】
        // 読み書きともにカレントディレクトリ (.)
        readBaseDir = new File(".");
        writeBaseDir = new File(".");
        this.jarPath = ""; 
    }

    // --- 各種パスの紐付け割当 ---

    // AozoraEpub3が内部で参照する template、web、profiles の「読み込み元」パスの決定
    // ⚠️もし profiles や web にユーザーが直接ファイルを新規保存・変更する場合は writeBaseDir に変更してください
    this.configPath = determineConfigPath(propFileName);
    this.cachePath = new File(writeBaseDir, ".cache");
    this.webConfigPath = new File(readBaseDir, "web");
    this.profilePath = new File(readBaseDir, "profiles");

    // 必要なディレクトリの自動生成(書き込み先のみ)
    if (!writeBaseDir.exists()) writeBaseDir.mkdirs();
    if (!this.cachePath.exists()) this.cachePath.mkdir();

    // --- 設定ファイル (Properties) の読み込み ---
    // 設定ファイルは「書き込みが発生する」ため、writeBaseDir から読み込みます
    File propFile = new File(writeBaseDir, this.propFileName);
    props = new Properties();
    try {
        if (Files.exists(propFile.toPath())) {
            try (FileInputStream fos = new FileInputStream(propFile)) {
                props.load(fos);
            }
        }
    } catch (Exception e) {
        throw new RuntimeException(e);
    }

    String path = props.getProperty("LastDir");
    if (path != null && !path.isEmpty()) this.currentPath = new File(path);
}

📝 補足:テンプレート読み込みエラーの回避

this.jarPath を用いて相対パスでテンプレートを探す処理がある場合、環境に応じて this.jarPath = "app/" と動的に設定することでエラーを回避できます。詳細や追加のトラブルシューティングは、参照ドキュメントをご確認ください。 [1, 4]

[1] https://blog1.mammb.com [2] https://docs.oracle.com [3] https://nowokay.hatenablog.com [4] https://docs.oracle.com [5] https://bugs.openjdk.org [6] https://www.reddit.com

chatgptに再検討させる

方向性は合っていますが、このままだと profiles と web が AppData に移らないので、「設定をAppDataへ切り替える」という目的なら少し修正した方がいいです。

特にここです。

this.webConfigPath = new File(readBaseDir, "web");
this.profilePath = new File(readBaseDir, "profiles");

これは jpackage 時には

app/web
app/profiles

を指します。

一方、

this.cachePath = new File(writeBaseDir, ".cache");

と

new File(writeBaseDir, this.propFileName)

は

%APPDATA%\AozoraEpub3\.cache
%APPDATA%\AozoraEpub3\設定ファイル

になるので、ここは意図通りです。

目的別に分けるなら

おそらくこうするのが自然です。

インストール先
  app/
    AozoraEpub3.jar
    web/             ← アプリ付属の読み込み専用
    profiles/        ← アプリ付属の初期プロファイル

%APPDATA%
  AozoraEpub3/
    *.properties     ← ユーザー設定
    .cache/           ← キャッシュ
    profiles/         ← ユーザーが保存・変更するプロファイル

つまり、読み込み専用の web は app に残し、ユーザー設定の profiles は AppData にするのがよいです。

例えば、

this.webConfigPath = new File(readBaseDir, "web");
this.profilePath = new File(writeBaseDir, "profiles");

です。

そして、

if (!this.profilePath.exists()) {
    this.profilePath.mkdirs();
}

も必要になります。


もう一つ重要な点

この部分:

readBaseDir = new File("app");
this.jarPath = "app" + File.separator;

は、現在の作業ディレクトリが jpackage の想定場所であることに依存しています。

jpackage の exe を普通に起動する場合にはたまたま動いても、

C:\Users\xxx> C:\Program Files\AozoraEpub3\AozoraEpub3.exe

のように別のカレントディレクトリから起動した場合、

new File("app")

は C:\Users\xxx\app を指す可能性があります。

そのため、jpackage のインストール先を基準に app を求める方が安全です。

ただし、今回の目的が「既存コードをなるべく変更せず、書き込み先だけ AppData に移す」なら、まずはそこまで大きく変更しなくてもいいです。


APPDATA の扱い

ここは概ね問題ありません。

String appData = System.getenv("APPDATA");
if (appData != null) {
    writeBaseDir = new File(appData, "AozoraEpub3");
} else {
    writeBaseDir = new File(System.getProperty("user.home"), ".AozoraEpub3");
}

ただ、空文字の可能性も考えるなら、

if (appData != null && !appData.isEmpty()) {

くらいにしておくと安全です。

また、

writeBaseDir.mkdirs();

の戻り値を確認した方がよいです。設定を書けない状態でも、そのまま起動してしまうためです。


最小変更ならこう

今回の目的だけなら、私はまず以下程度にします。

if (isJPackageEnv) {
    // 読み込み用
    readBaseDir = new File("app");
    this.jarPath = "app" + File.separator;

    // 書き込み用
    String appData = System.getenv("APPDATA");
    if (appData != null && !appData.isEmpty()) {
        writeBaseDir = new File(appData, "AozoraEpub3");
    } else {
        writeBaseDir = new File(System.getProperty("user.home"), ".AozoraEpub3");
    }
} else {
    // 従来のJAR配布時
    readBaseDir = new File(".");
    writeBaseDir = new File(".");
    this.jarPath = "";
}

if (!writeBaseDir.exists() && !writeBaseDir.mkdirs()) {
    throw new RuntimeException("設定フォルダを作成できません: " + writeBaseDir);
}

this.configPath = determineConfigPath(propFileName);
this.cachePath = new File(writeBaseDir, ".cache");
this.webConfigPath = new File(readBaseDir, "web");

// profilesもユーザー設定ならAppDataへ
this.profilePath = new File(writeBaseDir, "profiles");

if (!this.cachePath.exists()) {
    this.cachePath.mkdirs();
}
if (!this.profilePath.exists()) {
    this.profilePath.mkdirs();
}

そして、

File propFile = new File(writeBaseDir, this.propFileName);

は現在のままでOKです。

ポイントは profiles をどう扱うかです。

  • profiles がアプリに付属する初期データ → readBaseDir
  • profiles にユーザーが保存・変更する → writeBaseDir

という分け方にしてください。

なお、元コードで profilePath に対して listFiles() したりプロファイルを保存したりしているなら、ユーザー設定として扱うなら writeBaseDir に移すのが自然です。

このコードはかなり整理されていますが、「jpackage版では設定をAppDataに保存する」ことが目的なら、determineConfigPath() の考え方を少し変えた方がいいです。

特に問題なのは、現在のコードが「書き込み可能ならアプリフォルダを使う」という設計になっている点です。

1. 最大の問題:jpackageでもアプリフォルダを使う可能性がある

ここです。

if (Files.exists(iniPath) || canWrite(appDir)) {
    return appDir.toString();
}

これだと、

インストール先に古い設定ファイルが存在する

場合、

AppDataではなくインストール先

を使い続けます。

今回の目的が

jarがあるフォルダーに保存していた設定をAppDataへ切り替える

なら、jpackage版では書き込み可能かどうかに関係なくAppDataを使う方が明確です。

canWrite() 自体も不要になります。


2. determineConfigPath() はもっと単純にできる

今回なら、こういう考え方で十分です。

private String determineConfigPath(String propFileName) {
    String appData = System.getenv("APPDATA");

    // jpackageで起動している場合はAppData
    if (System.getProperty("jpackage.app-path") != null
            && appData != null && !appData.isEmpty()) {
        Path appDataDir = Path.of(appData, "AozoraEpub3");
        try {
            Files.createDirectories(appDataDir);
        } catch (IOException e) {
            throw new RuntimeException(e);
        }
        return appDataDir.toString();
    }

    // 通常のJAR版は従来どおり
    return new File(".").getAbsolutePath();
}

これなら非常に明確です。

jpackage EXE
    ↓
%APPDATA%\AozoraEpub3

通常のJAR
    ↓
カレントディレクトリ

という動作になります。


3. determinedPath.startsWith(appData) も不要

現在は、

String appDataEnv = System.getenv("APPDATA");

if (appDataEnv != null && determinedPath.startsWith(Path.of(appDataEnv).toString())) {
    this.jarPath = "app" + File.separator;
} else {
    this.jarPath = "";
}

となっています。

これは「設定保存先がAppDataだったらjpackage版」と判断していますが、本来は実行形式を直接判定した方がいいです。

すでに使っている、

System.getProperty("jpackage.app-path") != null

で十分です。

つまり、

boolean isJPackageEnv =
        System.getProperty("jpackage.app-path") != null;

if (isJPackageEnv) {
    this.jarPath = "app" + File.separator;
} else {
    this.jarPath = "";
}

でいいです。


4. profiles をどこに置くかは仕様次第

現在は、

this.webConfigPath = new File(this.jarPath + "web");
this.profilePath = new File(this.jarPath + "profiles");

なのでjpackage版では、

app/web
app/profiles

です。

これは、

  • web → アプリに付属する設定・テンプレート
  • profiles → アプリに付属する初期プロファイル

という扱いなら問題ありません。

ただし、ユーザーがプロファイルを追加・変更する仕様なら、

this.profilePath = new File(baseDir, "profiles");

にして、

%APPDATA%\AozoraEpub3\profiles

にした方がいいです。

個人的には、現在のAozoraEpub3の使い方を考えると、ユーザーがプロファイルを保存するならAppData側にするのが自然です。


5. jarPath = "app/" は少し注意

ここも、

this.jarPath = "app" + File.separator;

は相対パスです。

つまり、

new File(this.jarPath + "web")

は、

現在の作業ディレクトリ/app/web

です。

jpackageのexeを通常の方法で起動している場合は動く可能性が高いですが、カレントディレクトリに依存しています。

もし web や profiles をjpackageの app フォルダから読むなら、本当は絶対パスを作った方が安全です。

ただ、ここは既存コードへの変更範囲が大きくなるので、今回の「設定をAppDataへ移す」という目的だけなら、まず触らなくてもいいと思います。


6. determineConfigPath() のjpackage判定は今のコードと矛盾している

現在は、

// jpackage構成(app/lib配下)の場合は2階層上に戻す
if (baseDir.getName().equals("lib") && baseDir.getParentFile().getName().equals("app")) {
    baseDir = baseDir.getParentFile().getParentFile();
}

となっています。

ここは少し危険です。

jpackageの構成を、

AozoraEpub3/
 ├─ app/
 │   └─ AozoraEpub3.jar
 ├─ runtime/
 └─ AozoraEpub3.exe

のように考えるなら、通常JarのCodeSourceが app 内を指す場合、

baseDir

はすでに

AozoraEpub3/app

です。

その場合、

baseDir.getName().equals("lib")

にはなりません。

つまり、この判定は現在の実際のjpackage構成と合っているか確認した方がいいです。


7. 今回なら「判定」と「保存先」を分離した方がいい

一番分かりやすいのは、

boolean isJPackageEnv =
        System.getProperty("jpackage.app-path") != null;

String determinedPath = determineConfigPath(this.propFileName);
File baseDir = new File(determinedPath);

if (isJPackageEnv) {
    this.jarPath = "app" + File.separator;
} else {
    this.jarPath = "";
}

としてしまうことです。

そして determineConfigPath() は、

private String determineConfigPath(String propFileName) {
    boolean isJPackageEnv =
            System.getProperty("jpackage.app-path") != null;

    if (isJPackageEnv) {
        String appData = System.getenv("APPDATA");

        if (appData == null || appData.isEmpty()) {
            throw new IllegalStateException("APPDATAが取得できません");
        }

        Path appDataDir = Path.of(appData, "AozoraEpub3");

        try {
            Files.createDirectories(appDataDir);
        } catch (IOException e) {
            throw new RuntimeException(e);
        }

        return appDataDir.toString();
    }

    return new File(".").getAbsolutePath();
}

くらいにしてしまう方が、今回の目的には合っています。


結論

今のコードは、

「アプリフォルダに書けなければAppDataに逃がす」

という設計ならよくできています。

しかし今回やりたいのが、

「jpackage版からは、設定保存先を明確にAppDataへ移す」

なら、canWrite() を使った判定はむしろ不要です。

おすすめは、

jpackage版
    設定 → %APPDATA%\AozoraEpub3
    キャッシュ → %APPDATA%\AozoraEpub3\.cache
    ユーザープロファイル → %APPDATA%\AozoraEpub3\profiles
    web等の付属ファイル → インストール先 app\web

通常JAR版
    設定 → 従来どおり
    キャッシュ → 従来どおり
    profiles → 従来どおり

という明確な分離です。

特に既存の古い .properties がインストール先に残っていても、それを見ずにAppDataへ切り替えることが、今回の移行では重要です。