PyFletRakugaki_jikougamen12.png
PyFletRakugaki
V1.2 2026/01/02
ちょっとした落書きをするソフトです。
マウスドラッグで1~20ピクセルの幅でカラフルな色の線を描くことができます。
♪キャンバスをクリアできます。
♪白紙のキャンバスにマウスドラッグで線を描画できます。
♪画像をロードして背景にしてその上に重ねてマウスドラッグで線を描画できます。
♪線の描画を1ステップずつもとに戻すことができます。
♪キャンバスのスクリーンショットを撮ってクリップボードにコピーしたり、ファイル
保存したりできます。
♪線をたくさん描いて描画処理が重くなった場合に「遅延回復」ボタンをクリックする
ことで、処理の軽さを初期状態にもどすことができます。

PyFletRakugaki12.zip
readme_PyFletRakugaki.pdf

ソースコード
注:Fletのバージョンは0.80.0でコンパイルしてください。

#PyFletRakugaki.py キャンバスに落書き

# flet                   0.80.0
# flet-desktop           0.80.0
# flet-video             0.80.0

import flet as ft
import flet.canvas as cv
import pyautogui
import threading
import io
import sys
import win32clipboard
import time
import os
import shutil


def main(page: ft.Page):
    page.title = 'PyFletRakugaki V1.2'
   
### class ############################
    class ten: #マウスの座標点
        x = 0; y = 0
   
    class sen: #線の太さと色
        futosa = 3
        iro = ft.Colors.BLACK
       
    class kaisi: #イメージロードの有無
        non = 0 #背景画像ロードなしで開始したら1にする
        load = 0 #背景画像ロードして開始したら1にする
   
    class file: #ファイル名
        path = ''
       
    class kaz: #遅延回復で使うtemp画像に付ける連番
        zobun = 0
       
    class myFile: #画像保存でつかうファイル名
        path = ''
       
    class majin: #スクリーンショットを取る範囲のwindow全体からの余白マージン
        #left = 17; top = 160; width = 36; height = 175
        left = 19; top = 162; width = 40; height = 180

### スクリーンショットエリア設定 ###############################################
    def eriaSetei():
        return pyautogui.screenshot(region=
            (int(page.window.left + majin.left), int(page.window.top + majin.top),
                int(page.window.width - majin.width), int(page.window.height - majin.height)))
       

### color button 線の色を変更するボタン##########################################################
    def color_clicked(e):
        e.control.selected = not e.control.selected
        sen.iro = e.control.icon_color
       
    colors = [ft.Colors.BLACK, ft.Colors.BLUE, ft.Colors.BROWN, ft.Colors.CYAN, ft.Colors.GREEN,
        ft.Colors.GREY, ft.Colors.INDIGO, ft.Colors.LIME, ft.Colors.ORANGE, ft.Colors.PINK,
        ft.Colors.PURPLE, ft.Colors.RED, ft.Colors.TEAL, ft.Colors.WHITE, ft.Colors.YELLOW]

    icon_buttons = [
        ft.IconButton(
            tooltip = '線の色',
            icon = ft.Icons.FAVORITE,
            icon_color = color,
            style = ft.ButtonStyle(bgcolor = {'': ft.Colors.GREY_100}),
            on_click = color_clicked )
        for color in colors ]
   
### slider 線の太さを変更するスライダー###############################################################   
    slider = ft.Slider(
        min = 1, max = 20, divisions = 20, label = '{value}', value = 3, expand = True, tooltip = '線の太さ')
   
### start and update マウスドラッグの開始とドラッグ##################################################
    def start(e: ft.DragStartEvent): #マウスドラッグ開始
        ten.x = e.local_position.x; ten.y = e.local_position.y
   
    def update(e: ft.DragUpdateEvent): #マウスドラッグ中
        area.shapes.append(           
            cv.Line(ten.x, ten.y, e.local_position.x, e.local_position.y, paint = ft.Paint(
                stroke_width = slider.value, color = sen.iro, stroke_cap = ft.StrokeCap.ROUND)))
        try: area.update()
        except: pass
        #page.update()
        ten.x = e.local_position.x; ten.y = e.local_position.y

### area 線を描くエリア###########################################################################
    area = cv.Canvas(       
        #[cv.Color(color = ft.Colors.WHITE)], #←これを指定すると背景が白い色になる、指定しなければ背景は透明。
        content = ft.GestureDetector(on_pan_start = start, on_pan_update = update, drag_interval = 20),
        expand = True)

### undo 描画をもとに戻す#######################################################################
    def undo_last_shape(e):
        try:area.shapes.pop() #描画エリアでドラッグした線を1ステップ戻す
        except: pass
        page.update()

    undo_button = ft.Button('描画をもとに戻す', on_click=undo_last_shape)
   
### スクリーンショットの画像をクリップボードへコピーする ##################################################
    def copy_to_clipboard():
        def handle_close(e):           
            page.pop_dialog() #最前面のダイアログを閉じる
     
        copyShot = eriaSetei()
        original_image = copyShot #Image.open('screenshot.png') #スクリーンショット画像を開く       
        output = io.BytesIO()
        original_image.convert('RGB').save(output, 'BMP') #メモリストリームにBMP形式で保存してから読み出す
        data = output.getvalue()[14:]
        output.close()
        #クリップボードをクリアして、データをセットする
        win32clipboard.OpenClipboard();
        win32clipboard.EmptyClipboard(); win32clipboard.SetClipboardData(win32clipboard.CF_DIB, data)
        win32clipboard.CloseClipboard()
   
        dlg = ft.AlertDialog(modal=True, title=ft.Text('PyFletVideo'),
            content=ft.Text(f'画像をクリップボードへコピーしました。'),
            actions=[ft.TextButton("閉じる", on_click = handle_close)])       
        page.show_dialog(dlg)
        page.update()   
   
    copy_button = ft.Button('画像コピー', on_click = copy_to_clipboard, tooltip = 'スクリーンショットをクリップボードへコピーする')

### 遅延回復処理 ####################################################
    #cv.Canvasのshapes.appendしていくうちに処理がだんだん重くなってくるので、いったんスクリーン画像をファイルにセーブしてから
    #shapes.clear()したら、セーブしておいたスクリーン画像を表示させて、ふたたび遅延なく描画できるようにする。   
    def tien_kaifuku():
        tempShot = eriaSetei()
        try: os.makedirs('temp', exist_ok=True) #PyFletRakugakiの実行カレントフォルダ配下に'temp'フォルダを作成する
        except: pass
        image_path = 'temp/temp_screenshot' + str(kaz.zobun) + '.png' #画像保存ファイル名に連番を付加する
        try: os.remove(image_path) #前の画像保存ファイルを削除する
        except: pass
        kaz.zobun += 1 #連番を1増やす
        image_path = 'temp/temp_screenshot' + str(kaz.zobun) + '.png' #連番が1増えたファイル名
        tempShot.save(image_path) #連番が1増えたファイル名で画像を保存する
        area.shapes.clear() #キャンバスが増え続けたappendで重くなっているので、クリアして軽くする
        try: page.controls.pop(2) #pageコントロールの「描画エリア」を削除する
        except: pass       
        page.add(ft.Stack([ft.Image(src = image_path), area], expand=True)) #連番が1増えたファイル名の画像を表示させる
        page.update()
       
    kaifuku_button = ft.Button('遅延回復', on_click = tien_kaifuku, tooltip = '処理が重くなった場合にクリックしてください')     

### 白紙のスクリーンショットを撮って'canvas_screenshot.png'というファイル名でカレントディレクトリに保存#####
    def white_screenshot():
        scShot = eriaSetei()
        myFile.path = 'white_screenshot.png'
        scShot.save(myFile.path)
        myFile.path = 'canvas_screenshot.png'

### screenshot スクリーンショットを撮って'canvas_screenshot.png'というファイル名でカレントディレクトリに保存する#####
    def save_screenshot():       
        global screenshot       
        def handle_close(e):           
            page.pop_dialog() # 最前面のダイアログを閉じる
        screenshot = eriaSetei()
        screenshot.save(myFile.path)
        dlg = ft.AlertDialog(modal=True, title=ft.Text('PyFletVideo'),
            content=ft.Text(f'画像が {myFile.path} に保存されました。'),
            actions=[ft.TextButton("閉じる", on_click = handle_close)])       
        page.show_dialog(dlg)
        page.update()
       
    shot_button = ft.Button('スクリーンショット', on_click = lambda e: threading.Thread(target=save_screenshot()).start())

### image load カレントディレクトリの'open_image.png'画像を読み込んでエリアに表示する############   
    def image_load(e):       
        white_screenshot()
        try: page.controls.pop(2) #pageコントロールの「描画エリア」を削除する
        except: pass
        my_image = ft.Image(src = 'open_image.png')
        page.add(ft.Stack([my_image, area], expand=True))
       
    load_button = ft.Button('画像ロードして始める', on_click=image_load)

### non_load 背景画像読み込み無しで新規画像エリアを準備する ########################################
    def non_load(e):
        white_screenshot()
        try: page.controls.pop(2) #pageコントロールの「描画エリア」を削除する
        except: pass
        #print(f'mae 現在のページコントロール: {page.controls}') #チェック用          
        page.add(area)
        #print(f'ato 現在のページコントロール: {page.controls}') #チェック用
       
    non_load_button = ft.Button('画像ロードなしで始める', on_click = non_load)

### clear_canvas 描画エリアの線をクリアする######################################################
    def clear_canvas(e):
        try: area.shapes.clear(); area.update()           
        except: pass
        try: page.controls.pop(2) #pageコントロールの「描画エリア」を削除する
        except: pass
        try:
            my_image = ft.Image(src = 'white_screenshot.png')
            page.add(ft.Stack([my_image, area], expand=True))
        except:pass
        page.update()
       
    clear_button = ft.Button('描画をクリアする', on_click = clear_canvas)       

### page.add ページにボタンを配置する
    # 描画エリアクリア、背景画像なしの新規エリア、背景画像読み込みの新規エリア、描画を元に戻す、画像コピー、スクリーンショット、遅延回復
    # 線の太さスライダー、色を選ぶカラーボタン
    page.add(
        ft.Row([clear_button, non_load_button, load_button, undo_button, copy_button, shot_button, kaifuku_button]),
        ft.Row([ft.Container(content = slider, width = 200, padding = 10), (ft.Row(controls = icon_buttons, wrap = True))]))
   
    if page.window.visible:
        pass
        #ここにwindowが開始されたすぐの処理を書く ###将来用・未使用###
        #print("Window is now shown (visible)")

    def window_on_close(e):
        try:shutil.rmtree('temp') #遅延回復のために作成したtempフォルダを削除する
        except: pass
   
### app アプリケーションの開始####################################################################### 
    page.on_close = window_on_close
ft.app(target=main)