PyVideoPlayer_jikougamen13.png
PyVideoPalyer
V1.4 2026/01/05
シンプルな動画再生プレイヤーです。
♪お使いのパソコンに設定されているコーデックで再生できるメディアを再生できます。
【メディアファイルの開き方】
1. [Open]でメディアファイルを開いたら、Play/Pauseで再生/ポーズしてください。
2. 関連付け等の引数を伴う起動ができます。例:メディアファイルをPyVideoPlayer.exeから開く
3. [Drop]のクリックでドロップエリアが表示されますので、そこにwindowsエクスプローラ等から
 メディアファイルをドラッグ&ドロップしてください。すると、そのメディアファイルを再生できます。
 ([Drop]をクリックするたびにドロップエリアの表示/非表示ができます)
♪再生時間プログレスバーのクリックでその位置へシーク、マウスホイール(向こう側:巻き戻し、手前側:早送り)
 ができます。
♪[Shot]でスクリーンショットを撮る事ができます。撮った画像ファイルはPyVideoPlayer.exeと同じフォルダに
 canvas_screenshot.pngというファイル名で保存されます。
♪[Normal]ボタンで画面を標準サイズに戻す事ができます。
♪[Play/Pause]再生/ポーズ、音量調整、再生速度変更(0.2~3.2倍)、[Rate 1X]で再生速度を標準にすることができます。
♪プレイヤー画面では、再生/ポーズ(ボタンorスペースキー)、音量、ミュート、プログレスバーで再生位置シーク
 (クリックorドラッグor←キーor→キーで再生位置変更)、
フルスクリーン切り替え(右下の拡大ボタン、画面のダブルクリックでOn/Off、Escキーでもとの大きさの画面に戻す)
ができます。

PyVideoPlayer14.zip
readme_PyVideoPlayer.pdf

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

#PyVideoPlayer.py  V1.4

# Python 3.10.11
# flet                      0.28.3
# flet-cli                  0.28.3
# flet-desktop              0.28.3
# flet-web                  0.28.3

import flet as ft
import threading
import pyautogui
import asyncio
import sys
import os
from pathlib import Path
import tkinter as tk
from tkinterdnd2 import *
import tkinter.messagebox as messagebox

def main(page: ft.Page):
    page.title = 'PyVideoPlayer V1.4'
   
### class ############################
    class Root: #root.mainloop()している間(ドロップエリアが機能している間)はTrueにしておく
        loop = True
   
    class dropShow: #ドロップウインドウの表示/非表示
        On = False
       
### Flet終了ボタンをクリックした時の処理 ##########################################
    def goodbye(e): #閉じる処理       
        page.window.destroy()
        sys.exit()

    def window_event(e): #Fletウインドウの閉じるボタンが押されたときの処理
        if e.data == 'close':           
            if Root.loop: #まだドロップエリアが機能していたら、まずこのドロップエリアを閉じること。
                page.open(confirm_dialog2) #ドロップエリア終了確認ダイアログを表示する
            else:
                page.open(confirm_dialog) #Flet終了確認ダイアログを表示する
            page.update()   

    page.window.on_event = window_event #Fletウインドウのイベントを設定する   
    page.window.prevent_close = True #Fletウインドウを閉じさせないようにする
    def yes_click(e): #閉じるをはいと答えた場合
        goodbye(e) #閉じる処理をする

    def drop_yes_click(e): #ドロップエリア閉じるをはいと答えた場合(はい以外は無い)
        drop_button.disabled = True
        page.update()
        Root.loop = False       
        page.close(confirm_dialog2) #ドロップ確認ダイアログを閉じる
        root.quit()
        root.destroy() #rootループ(ドロップエリア)を閉じる
   
    confirm_dialog = ft.AlertDialog( #終了確認ダイアログ(はい以外無い)
        modal = True,
        title = ft.Text('PyVideoPlayer 終了確認'),
        content = ft.Text('PyVideoPlayerを終了します(終了には少し時間がかかります)'),
        actions = [ft.ElevatedButton('はい、終了します', on_click=yes_click)],
        actions_alignment = ft.MainAxisAlignment.END,) #ボタン類を右端に表示させる
   
    confirm_dialog2 = ft.AlertDialog( #rootループが閉じられていない場合の終了確認ダイアログ(はい以外無い)
        modal = True,
        title = ft.Text('PyVideoPlayer ドロップエリアを閉じる'),
        content = ft.Text('PyVideoPlayerを閉じるためには、先にドロップエリアを閉じる必要があります。'),
        actions = [ft.ElevatedButton('はい、ドロップエリアを閉じます', on_click = drop_yes_click)],
        actions_alignment = ft.MainAxisAlignment.END,) #ボタン類を右端に表示させる  

### ビデオをロードする ###################################################################################### 
    def dialogFile(e: ft.FilePickerResultEvent): #ファイルオープンダイアログからビデオファイルを開いた場合
        asyncio.run(open_video(e.files[0].path)) #ファイルオープンダイアログのファイルパスを渡して再生する
   
    async def open_video(path): #pathにファイルパスを渡してビデオファイルを開いて再生する      
        global video, task       
        picked_video = ft.VideoMedia(path) #"01namida.mpg")
        video = ft.Video(expand = True, aspect_ratio = 16 / 9, autoplay = True,
            filter_quality = ft.FilterQuality.HIGH, playlist = [picked_video], playlist_mode = ft.PlaylistMode.NONE)       
        try: page.controls.pop(1) #pageコントロールの「ビデオエリア」を削除する
        except: pass
        page.add(video) #pageコントロールに「ビデオエリア」を追加する
        video.volume = 50; vol_slider.value = 50
        video.playback_rate = 1; rate_slider = 1.0
        page.title = path
        volText.value = 'Volume='+str(video.volume)[:4] #表示文字数を4文字に制限
        rateText.value = 'Rate='+str(video.playback_rate)[:4] #表示文字数を4文字に制限      
        page.update()
       
### ファイルオープンダイアログを表示する #################################################################################
    file_picker = ft.FilePicker(on_result = dialogFile); page.overlay.append(file_picker)
    pick_button = ft.ElevatedButton("Open", icon=ft.Icons.VIDEO_FILE_OUTLINED,
        on_click = lambda _: file_picker.pick_files(allow_multiple=False,
            file_type=ft.FilePickerFileType.ANY), tooltip = 'ビデオファイルオープン')   

### スライダーでボリュームを変更する ##############################################################
    def volume_change(e):       
        video.volume = e.control.value
        volText.value = 'Volume='+str(video.volume)[:4] #表示文字数を4文字に制限
        page.update()

    vol_slider = ft.Slider(min = 0, value = 50, max = 100, divisions = 20, width = 200, on_change = volume_change, tooltip = '音量')
       
### スライダーで再生速度rateを変更する ####################################################################       
    def rate_change(e):       
        video.playback_rate = e.control.value
        rateText.value = 'Rate='+str(video.playback_rate)[:4] #表示文字数を4文字に制限
        page.update()
   
    rate_slider = ft.Slider(min = 0.2, value = 1.0, max = 3.2, divisions = 30, width = 200, on_change = rate_change, tooltip = "再生速度")

### 再生速度を標準1.0にする ###############################################################
    def rate1(e):
        rate_slider.value = 1.0
        rate_slider.update()
        #print(f'rate={rate_slider.value}')
        video.playback_rate = 1.0
        rateText.value = 'Rate='+str(video.playback_rate)[:4] #表示文字数を4文字に制限
        page.update()
       
### 再生/ポース交互切り替え ############################################################
    def play(e):
        video.play_or_pause()       
       
### screenshot スクリーンショットを撮って'canvas_screenshot.png'というファイル名でカレントディレクトリに保存する ################
    def save_screenshot(e):       
        screenshot = pyautogui.screenshot(region=
                    (int(page.window.left + 10), int(page.window.top + 110), int(page.window.width - 20), int(page.window.height - 120)))
        file_path = 'canvas_screenshot.png'
        screenshot.save(file_path)
        page.open(ft.SnackBar(ft.Text(f'画像が {file_path} に保存されました。'), open = True))
        page.update()

    shot_button = ft.ElevatedButton(tooltip = 'スクリーンショットを保存',
        text = 'Shot', on_click=lambda e: threading.Thread(target=save_screenshot(e)).start())

### プログレスバーをホイールした時の処理 ##########################################################
    def bar_scroll(e):
        # イベントオブジェクトから座標を取得
        sy = e.scroll_delta_y #マウスホイール変化はy方向(x方向は0)で、手前が+ 向こうが-
        gen = video.get_current_position()
        if sy > 0: #手前側:早送り
            yy = gen + 10000           
        elif sy < 0: #向こう側:巻き戻し
            yy = gen - 10000
        #page.title = yy #チェック用
        #local_x = e.local_x; local_y = e.local_y
        #global_x = e.global_x; global_y = e.global_y       
        if yy < 0:
            yy = 0
        elif yy > video.get_duration():
            yy = video.get_duration()
        video.seek(int(yy)) #ビデオを所定の再生位置にシークさせる
        #print(f"barローカル座標: x={local_x:.2f}, y={local_y:.2f}\n")
        #print(f"barグローバル座標: x={global_x:.2f}, y={global_y:.2f}")       
        try:genzai = video.get_current_position() / video.get_duration()
        except:genzai = 0;
        #print(f'genzai={genzai}') #チェック用
        progress_bar.value = genzai           
        page.update()  # UIを更新

### プログレスバーをクリックした時の処理 ##########################################################
    def bar_click(e):
        # イベントオブジェクトから座標を取得
        video.seek(int(e.local_x / page.width * video.get_duration())) #ビデオを所定の再生位置にシークさせる
        gen = video.get_current_position()      
        try:genzai = video.get_current_position() / video.get_duration()
        except:genzai = 0;       
        progress_bar.value = genzai           
        page.update()  # UIを更新
       
### ファイルをドロップした時の処理 ##################################################################################################
    def drop_file(event):
        #ドロップされたファイル名に半角の空白が含まれる場合になぜか前後に{}がつく(なお全角の空白なら問題無い)
        #例: {C:/myDelphi/miniDPlay/eizosanpul/02_めちゃイケ AKBセンターバカ.flv}
        #なのでリストの2番目の要素(インデックス1)から最後から2番目の要素(インデックス-1)までを取り出す(スライスする)      
        if event.data.startswith('{'): event.data = event.data[1:-1]
        else: pass
        asyncio.run(open_video(event.data)) #メディアファイルを開いて再生する

### ドロップエリアの表示/非表示 ###################################################
    def openDrop(e):
        if dropShow.On:
            dropShow.On = False
            root.withdraw() #ウィンドウを非表示にする
        else:
            dropShow.On = True
            root.deiconify()
            xx = int(page.window.left + 750); yy = int(page.window.top + 100)
            root.geometry(f'200x100+{xx}+{yy}') #ウィンドウを所定の位置に表示する

### 標準画面に戻す(再生画面でフルスクリーンにしたのをもとに戻した時タイトルバーがデスクトプ画面の左上端にかくれてしまうから) #####
    def normalWindow(e):
        page.window.left = 100; page.window.top = 100; page.window.width = 1000; page.window.height = 500
        page.update()

### ボタン類を配置する ##################################################################################################
    #1段目:ファイルオープン、再生/ポーズ、ボリューム、再生速度標準、再生速度、標準画面、スクリーンショット、ドロップエリア、音量表示、再生速度表示
    #2段目:再生時間プログレスバー(クリックでその位置へシーク、ホイール(向こう側)で前へ戻る/(手前側)で先へ進む)
    progress_bar = ft.ProgressBar(value = 0, height = 8, expand = True, tooltip = 'シーク(クリックorホイール)')
    #プログレスバーにはマウスクリックやホイールイベント機能がないため、ft.GestureDetectorでラップする
    clickable_bar = ft.GestureDetector(mouse_cursor=ft.MouseCursor.CLICK, expand = True, content = progress_bar,
        on_scroll = bar_scroll, on_tap_down = bar_click) #(複数のイベント機能をもたせることも可)
    volText = ft.Text('Volume'); rateText = ft.Text('Rate')
    play_button = ft.ElevatedButton('Play/Pause', on_click = play, tooltip = '再生/ポーズ')
    rate1_button = ft.ElevatedButton('Rate 1X', on_click = rate1, tooltip = '再生速度を標準1.0にする')
    drop_button = ft.ElevatedButton('Drop', on_click = openDrop, tooltip = 'ドロップウインドウ On/Off')
    hyojun_button = ft.ElevatedButton('Normal', on_click = normalWindow, tooltip = '標準画面に戻す')
    page.add(ft.Column([
        ft.Row([pick_button, play_button, vol_slider, rate1_button, rate_slider, hyojun_button, shot_button, drop_button, volText, rateText]),
        ft.Container(content = clickable_bar, height = 8)]))   
   
### 関連付けやPyVideoPlayer.exeにメディアファイルをドロップなどして引数を伴うアプリの起動の場合 #############################################
    if len(sys.argv) > 1: #引数を伴う起動の場合
        file_path = sys.argv[1] #引数のファイル名を得る
        absolute_path = os.path.abspath(file_path) #絶対パスを得る
        if os.path.exists(absolute_path): #ファイルが存在していれば
            asyncio.run(open_video(absolute_path)) #関連付け等の引数のファイルを開いて再生する
           
### ファイルのドロップがFletでは装備されてないため、TkinterDnDを使ってファイルのドロップを受け入れる #####################################  
#     def rootClose(): #rootを閉じる時の処理
#         if messagebox.askokcancel('PyVideoPlayer ドロップエリア 閉じる確認', 'ドロップエリアを閉じると再開できませんが、本当に閉じますか?'):
#             Root.loop = False
#             root.destroy() # ウィンドウを閉じる
#         else: # キャンセルした場合はrootウィンドウを閉じない
#             pass  

    def quit_me(root):
        root.quit()
        root.destroy()

    root = TkinterDnD.Tk()   
    label = tk.Label(root, text = '↓ここにファイルをドロップ')
    label.pack()   
    root.title('PyVideoPlayer drop file')
    xx = int(page.window.left); yy = int(page.window.top);
    root.geometry(f'200x100+{xx}+{yy}') #後でドロップボタンをクリックした位置へウインドウを移動させる
    root.drop_target_register(DND_FILES)
    root.dnd_bind('<<Drop>>', drop_file)
    canvas = tk.Canvas(root, bg='white')
    canvas.pack(expand=True, fill=tk.BOTH)  
    root.attributes('-topmost', True) #最前面表示
    root.withdraw() #ウィンドウを非表示にする
    root.overrideredirect(True) #タイトルバーの非表示   
    #root.protocol("WM_DELETE_WINDOW", rootClose) # ウィンドウの閉じるボタン(×ボタン)が押された時の処理を設定 ※未使用  
    Root.loop = True
    root.protocol("WM_DELETE_WINDOW", lambda :quit_me(root))
    root.mainloop() #rootのメインウインドウをループ機能させる   

### プリケーションの開始#######################################################################
ft.app(target=main)