中級者向け No.13

ネットワークスキャナー

LAN内のデバイスをスキャンしてIPアドレス・MACアドレスを一覧表示するツール。socketとsubprocessの活用を学びます。

🎯 難易度: ★★☆ 📦 ライブラリ: tkinter(標準ライブラリ) ⏱️ 制作時間: 30〜90分

1. アプリ概要

LAN内のデバイスをスキャンしてIPアドレス・MACアドレスを一覧表示するツール。socketとsubprocessの活用を学びます。

このアプリは中級カテゴリに分類される実践的なGUIアプリです。使用ライブラリは tkinter(標準ライブラリ) で、難易度は ★★☆ です。

Pythonでは tkinter を使うことで、クロスプラットフォームなGUIアプリを簡単に作成できます。このアプリを通じて、ウィジェットの配置・イベント処理・データ管理など、GUI開発の実践的なスキルを習得できます。

ソースコードは完全な動作状態で提供しており、コピーしてそのまま実行できます。まずは実行して動作を確認し、その後コードを読んで仕組みを理解していきましょう。カスタマイズセクションでは機能拡張のアイデアも紹介しています。

GUIアプリ開発は、プログラミングの楽しさを実感できる最も効果的な学習方法のひとつです。アプリを作ることで、変数・関数・クラス・イベント処理など、プログラミングの重要な概念が自然と身についていきます。このアプリをきっかけに、オリジナルアプリの開発にも挑戦してみてください。

2. 機能一覧

  • ネットワークスキャナーのメイン機能
  • 直感的なGUIインターフェース
  • 入力値のバリデーション
  • エラーハンドリング
  • 結果の見やすい表示
  • キーボードショートカット対応

3. 事前準備・環境

ℹ️
動作確認環境

Python 3.10 以上 / Windows・Mac・Linux すべて対応

以下の環境で動作確認しています。

  • Python 3.10 以上
  • OS: Windows 10/11・macOS 12+・Ubuntu 20.04+

4. 完全なソースコード

💡
コードのコピー方法

右上の「コピー」ボタンをクリックするとコードをクリップボードにコピーできます。

追加インストール不要(標準ライブラリのみ使用)
app13.py
import tkinter as tk
from tkinter import ttk, messagebox
import socket
import subprocess
import threading
import ipaddress
import platform
import re
from datetime import datetime


class App13:
    """ネットワークスキャナー"""

    def __init__(self, root):
        self.root = root
        self.root.title("ネットワークスキャナー")
        self.root.geometry("820x580")
        self.root.configure(bg="#f8f9fc")
        self.scanning = False
        self._build_ui()
        self._detect_local_ip()

    def _build_ui(self):
        title_frame = tk.Frame(self.root, bg="#e65100", pady=10)
        title_frame.pack(fill=tk.X)
        tk.Label(title_frame, text="🔍 ネットワークスキャナー",
                 font=("Noto Sans JP", 15, "bold"),
                 bg="#e65100", fg="white").pack(side=tk.LEFT, padx=12)

        # スキャン設定
        cfg_frame = ttk.LabelFrame(self.root, text="スキャン設定", padding=10)
        cfg_frame.pack(fill=tk.X, padx=8, pady=6)

        tk.Label(cfg_frame, text="IPレンジ:").grid(row=0, column=0, sticky="w")
        self.range_var = tk.StringVar(value="192.168.1.0/24")
        ttk.Entry(cfg_frame, textvariable=self.range_var,
                  width=22, font=("Courier New", 11)).grid(
            row=0, column=1, padx=8, sticky="w")

        tk.Label(cfg_frame, text="タイムアウト(ms):").grid(
            row=0, column=2, sticky="w", padx=(16, 4))
        self.timeout_var = tk.IntVar(value=300)
        ttk.Spinbox(cfg_frame, from_=50, to=2000,
                    textvariable=self.timeout_var, width=6).grid(row=0, column=3)

        tk.Label(cfg_frame, text="スレッド数:").grid(
            row=0, column=4, sticky="w", padx=(16, 4))
        self.threads_var = tk.IntVar(value=50)
        ttk.Spinbox(cfg_frame, from_=1, to=200,
                    textvariable=self.threads_var, width=6).grid(row=0, column=5)

        self.port_var = tk.BooleanVar(value=False)
        ttk.Checkbutton(cfg_frame, text="ポートスキャン",
                        variable=self.port_var).grid(row=0, column=6, padx=16)

        self.scan_btn = ttk.Button(cfg_frame, text="▶ スキャン開始",
                                   command=self._toggle_scan)
        self.scan_btn.grid(row=0, column=7, padx=8)

        # プログレス
        self.progress_var = tk.IntVar(value=0)
        self.progress = ttk.Progressbar(self.root, variable=self.progress_var,
                                         maximum=100)
        self.progress.pack(fill=tk.X, padx=8)

        # 結果テーブル
        result_frame = ttk.LabelFrame(self.root, text="スキャン結果", padding=4)
        result_frame.pack(fill=tk.BOTH, expand=True, padx=8, pady=6)
        cols = ("ip", "hostname", "ping", "ports", "status")
        self.tree = ttk.Treeview(result_frame, columns=cols,
                                  show="headings", selectmode="browse")
        for c, h, w in [("ip", "IPアドレス", 130), ("hostname", "ホスト名", 200),
                         ("ping", "応答(ms)", 80), ("ports", "開放ポート", 200),
                         ("status", "状態", 70)]:
            self.tree.heading(c, text=h)
            self.tree.column(c, width=w, minwidth=40)
        v_sb = ttk.Scrollbar(result_frame, command=self.tree.yview)
        self.tree.configure(yscrollcommand=v_sb.set)
        v_sb.pack(side=tk.RIGHT, fill=tk.Y)
        self.tree.pack(fill=tk.BOTH, expand=True)
        self.tree.tag_configure("online", foreground="#27ae60")
        self.tree.tag_configure("offline", foreground="#bbb")

        # 下部: ローカル情報 + ツール
        bottom = tk.Frame(self.root, bg="#f8f9fc")
        bottom.pack(fill=tk.X, padx=8, pady=4)
        self.local_info_var = tk.StringVar(value="")
        tk.Label(bottom, textvariable=self.local_info_var,
                 bg="#f8f9fc", font=("Courier New", 10),
                 fg="#555").pack(side=tk.LEFT)

        tool_frame = tk.Frame(bottom, bg="#f8f9fc")
        tool_frame.pack(side=tk.RIGHT)
        tk.Label(tool_frame, text="ツール: ", bg="#f8f9fc").pack(side=tk.LEFT)
        self.tool_ip_var = tk.StringVar()
        ttk.Entry(tool_frame, textvariable=self.tool_ip_var, width=16).pack(side=tk.LEFT, padx=4)
        for text, cmd in [("Ping", self._ping_single),
                           ("ホスト名解決", self._resolve_host),
                           ("Traceroute", self._traceroute)]:
            ttk.Button(tool_frame, text=text, command=cmd).pack(side=tk.LEFT, padx=2)

        self.status_var = tk.StringVar(value="スキャン待機中")
        tk.Label(self.root, textvariable=self.status_var,
                 bg="#dde", font=("Arial", 9), anchor="w", padx=8
                 ).pack(fill=tk.X, side=tk.BOTTOM)

    def _detect_local_ip(self):
        try:
            s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
            s.connect(("8.8.8.8", 80))
            local_ip = s.getsockname()[0]
            s.close()
            # デフォルトレンジをセット
            parts = local_ip.rsplit(".", 1)
            self.range_var.set(f"{parts[0]}.0/24")
            hostname = socket.gethostname()
            self.local_info_var.set(
                f"ホスト: {hostname}  ローカルIP: {local_ip}")
            self.tool_ip_var.set(local_ip)
        except Exception:
            pass

    def _toggle_scan(self):
        if self.scanning:
            self.scanning = False
            self.scan_btn.config(text="▶ スキャン開始")
        else:
            self._start_scan()

    def _start_scan(self):
        try:
            network = ipaddress.ip_network(self.range_var.get().strip(),
                                           strict=False)
        except ValueError as e:
            messagebox.showerror("エラー", f"無効なIPレンジ: {e}")
            return
        self.tree.delete(*self.tree.get_children())
        self.scanning = True
        self.scan_btn.config(text="⏹ 停止")
        hosts = list(network.hosts())
        self.progress_var.set(0)
        threading.Thread(target=self._scan_range,
                         args=(hosts,), daemon=True).start()

    def _scan_range(self, hosts):
        total = len(hosts)
        found = 0
        semaphore = threading.Semaphore(self.threads_var.get())
        results = []
        lock = threading.Lock()

        def scan_host(ip):
            nonlocal found
            with semaphore:
                if not self.scanning:
                    return
                ip_str = str(ip)
                online, ms = self._ping(ip_str)
                if online:
                    hostname = self._get_hostname(ip_str)
                    ports = self._scan_ports(ip_str) if self.port_var.get() else ""
                    with lock:
                        found += 1
                        results.append((ip_str, hostname, ms, ports))
                        self.root.after(0, self._add_result,
                                        ip_str, hostname, ms, ports)

        threads = []
        for i, ip in enumerate(hosts):
            if not self.scanning:
                break
            t = threading.Thread(target=scan_host, args=(ip,), daemon=True)
            t.start()
            threads.append(t)
            pct = int((i + 1) / total * 100)
            self.root.after(0, self.progress_var.set, pct)
            self.root.after(0, self.status_var.set,
                            f"スキャン中: {i+1}/{total}  発見: {found}")

        for t in threads:
            t.join(timeout=2)

        self.root.after(0, self._scan_done, found)

    def _ping(self, ip):
        """pingを実行して(alive, ms)を返す"""
        timeout_s = self.timeout_var.get() / 1000
        try:
            param = "-n" if platform.system().lower() == "windows" else "-c"
            cmd = ["ping", param, "1", "-w" if platform.system().lower() == "windows"
                   else "-W", str(self.timeout_var.get()), ip]
            import time
            t0 = time.time()
            result = subprocess.run(cmd, capture_output=True,
                                     timeout=timeout_s + 1)
            ms = int((time.time() - t0) * 1000)
            alive = result.returncode == 0
            return alive, ms
        except Exception:
            # socketで接続チェック
            try:
                import time
                t0 = time.time()
                s = socket.create_connection((ip, 80),
                                              timeout=timeout_s)
                s.close()
                ms = int((time.time() - t0) * 1000)
                return True, ms
            except Exception:
                return False, 0

    def _get_hostname(self, ip):
        try:
            return socket.gethostbyaddr(ip)[0]
        except Exception:
            return ""

    def _scan_ports(self, ip):
        common_ports = [21, 22, 23, 25, 53, 80, 110, 143, 443,
                        445, 3306, 3389, 5432, 8080, 8443]
        open_ports = []
        for port in common_ports:
            try:
                s = socket.create_connection((ip, port), timeout=0.3)
                s.close()
                open_ports.append(port)
            except Exception:
                pass
        return ", ".join(str(p) for p in open_ports)

    def _add_result(self, ip, hostname, ms, ports):
        self.tree.insert("", "end",
                         values=(ip, hostname, f"{ms} ms", ports, "オンライン"),
                         tags=("online",))

    def _scan_done(self, found):
        self.scanning = False
        self.scan_btn.config(text="▶ スキャン開始")
        self.progress_var.set(100)
        self.status_var.set(
            f"スキャン完了  {found} 台発見  "
            f"({datetime.now().strftime('%H:%M:%S')})")

    def _ping_single(self):
        ip = self.tool_ip_var.get().strip()
        if not ip:
            return
        threading.Thread(target=self._do_ping_single,
                         args=(ip,), daemon=True).start()

    def _do_ping_single(self, ip):
        alive, ms = self._ping(ip)
        msg = f"{ip}: {'応答あり' if alive else '応答なし'} ({ms} ms)"
        self.root.after(0, lambda: messagebox.showinfo("Ping結果", msg))

    def _resolve_host(self):
        target = self.tool_ip_var.get().strip()
        if not target:
            return
        try:
            result = socket.gethostbyaddr(target)
            messagebox.showinfo("ホスト名解決",
                                f"ホスト名: {result[0]}\nエイリアス: {result[1]}\nIP: {result[2]}")
        except Exception as e:
            messagebox.showinfo("ホスト名解決", f"解決できませんでした: {e}")

    def _traceroute(self):
        ip = self.tool_ip_var.get().strip()
        if not ip:
            return
        win = tk.Toplevel(self.root)
        win.title(f"Traceroute — {ip}")
        win.geometry("500x300")
        txt = tk.Text(win, font=("Courier New", 10), bg="#1e1e1e",
                      fg="#ccc")
        txt.pack(fill=tk.BOTH, expand=True)

        def run():
            cmd = (["tracert", ip] if platform.system().lower() == "windows"
                   else ["traceroute", ip])
            try:
                proc = subprocess.Popen(cmd, stdout=subprocess.PIPE,
                                         stderr=subprocess.STDOUT,
                                         text=True)
                for line in proc.stdout:
                    win.after(0, txt.insert, tk.END, line)
            except Exception as e:
                win.after(0, txt.insert, tk.END, f"エラー: {e}")

        threading.Thread(target=run, daemon=True).start()


if __name__ == "__main__":
    root = tk.Tk()
    app = App13(root)
    root.mainloop()

5. コード解説

ネットワークスキャナーのコードを詳しく解説します。クラスベースの設計で各機能を整理して実装しています。

クラス設計とコンストラクタ

App13クラスにアプリの全機能をまとめています。__init__メソッドでウィンドウの基本設定を行い、_build_ui()でUI構築、process()でメイン処理を担当します。この分離により、各メソッドの責任が明確になりコードが読みやすくなります。

import tkinter as tk
from tkinter import ttk, messagebox
import socket
import subprocess
import threading
import ipaddress
import platform
import re
from datetime import datetime


class App13:
    """ネットワークスキャナー"""

    def __init__(self, root):
        self.root = root
        self.root.title("ネットワークスキャナー")
        self.root.geometry("820x580")
        self.root.configure(bg="#f8f9fc")
        self.scanning = False
        self._build_ui()
        self._detect_local_ip()

    def _build_ui(self):
        title_frame = tk.Frame(self.root, bg="#e65100", pady=10)
        title_frame.pack(fill=tk.X)
        tk.Label(title_frame, text="🔍 ネットワークスキャナー",
                 font=("Noto Sans JP", 15, "bold"),
                 bg="#e65100", fg="white").pack(side=tk.LEFT, padx=12)

        # スキャン設定
        cfg_frame = ttk.LabelFrame(self.root, text="スキャン設定", padding=10)
        cfg_frame.pack(fill=tk.X, padx=8, pady=6)

        tk.Label(cfg_frame, text="IPレンジ:").grid(row=0, column=0, sticky="w")
        self.range_var = tk.StringVar(value="192.168.1.0/24")
        ttk.Entry(cfg_frame, textvariable=self.range_var,
                  width=22, font=("Courier New", 11)).grid(
            row=0, column=1, padx=8, sticky="w")

        tk.Label(cfg_frame, text="タイムアウト(ms):").grid(
            row=0, column=2, sticky="w", padx=(16, 4))
        self.timeout_var = tk.IntVar(value=300)
        ttk.Spinbox(cfg_frame, from_=50, to=2000,
                    textvariable=self.timeout_var, width=6).grid(row=0, column=3)

        tk.Label(cfg_frame, text="スレッド数:").grid(
            row=0, column=4, sticky="w", padx=(16, 4))
        self.threads_var = tk.IntVar(value=50)
        ttk.Spinbox(cfg_frame, from_=1, to=200,
                    textvariable=self.threads_var, width=6).grid(row=0, column=5)

        self.port_var = tk.BooleanVar(value=False)
        ttk.Checkbutton(cfg_frame, text="ポートスキャン",
                        variable=self.port_var).grid(row=0, column=6, padx=16)

        self.scan_btn = ttk.Button(cfg_frame, text="▶ スキャン開始",
                                   command=self._toggle_scan)
        self.scan_btn.grid(row=0, column=7, padx=8)

        # プログレス
        self.progress_var = tk.IntVar(value=0)
        self.progress = ttk.Progressbar(self.root, variable=self.progress_var,
                                         maximum=100)
        self.progress.pack(fill=tk.X, padx=8)

        # 結果テーブル
        result_frame = ttk.LabelFrame(self.root, text="スキャン結果", padding=4)
        result_frame.pack(fill=tk.BOTH, expand=True, padx=8, pady=6)
        cols = ("ip", "hostname", "ping", "ports", "status")
        self.tree = ttk.Treeview(result_frame, columns=cols,
                                  show="headings", selectmode="browse")
        for c, h, w in [("ip", "IPアドレス", 130), ("hostname", "ホスト名", 200),
                         ("ping", "応答(ms)", 80), ("ports", "開放ポート", 200),
                         ("status", "状態", 70)]:
            self.tree.heading(c, text=h)
            self.tree.column(c, width=w, minwidth=40)
        v_sb = ttk.Scrollbar(result_frame, command=self.tree.yview)
        self.tree.configure(yscrollcommand=v_sb.set)
        v_sb.pack(side=tk.RIGHT, fill=tk.Y)
        self.tree.pack(fill=tk.BOTH, expand=True)
        self.tree.tag_configure("online", foreground="#27ae60")
        self.tree.tag_configure("offline", foreground="#bbb")

        # 下部: ローカル情報 + ツール
        bottom = tk.Frame(self.root, bg="#f8f9fc")
        bottom.pack(fill=tk.X, padx=8, pady=4)
        self.local_info_var = tk.StringVar(value="")
        tk.Label(bottom, textvariable=self.local_info_var,
                 bg="#f8f9fc", font=("Courier New", 10),
                 fg="#555").pack(side=tk.LEFT)

        tool_frame = tk.Frame(bottom, bg="#f8f9fc")
        tool_frame.pack(side=tk.RIGHT)
        tk.Label(tool_frame, text="ツール: ", bg="#f8f9fc").pack(side=tk.LEFT)
        self.tool_ip_var = tk.StringVar()
        ttk.Entry(tool_frame, textvariable=self.tool_ip_var, width=16).pack(side=tk.LEFT, padx=4)
        for text, cmd in [("Ping", self._ping_single),
                           ("ホスト名解決", self._resolve_host),
                           ("Traceroute", self._traceroute)]:
            ttk.Button(tool_frame, text=text, command=cmd).pack(side=tk.LEFT, padx=2)

        self.status_var = tk.StringVar(value="スキャン待機中")
        tk.Label(self.root, textvariable=self.status_var,
                 bg="#dde", font=("Arial", 9), anchor="w", padx=8
                 ).pack(fill=tk.X, side=tk.BOTTOM)

    def _detect_local_ip(self):
        try:
            s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
            s.connect(("8.8.8.8", 80))
            local_ip = s.getsockname()[0]
            s.close()
            # デフォルトレンジをセット
            parts = local_ip.rsplit(".", 1)
            self.range_var.set(f"{parts[0]}.0/24")
            hostname = socket.gethostname()
            self.local_info_var.set(
                f"ホスト: {hostname}  ローカルIP: {local_ip}")
            self.tool_ip_var.set(local_ip)
        except Exception:
            pass

    def _toggle_scan(self):
        if self.scanning:
            self.scanning = False
            self.scan_btn.config(text="▶ スキャン開始")
        else:
            self._start_scan()

    def _start_scan(self):
        try:
            network = ipaddress.ip_network(self.range_var.get().strip(),
                                           strict=False)
        except ValueError as e:
            messagebox.showerror("エラー", f"無効なIPレンジ: {e}")
            return
        self.tree.delete(*self.tree.get_children())
        self.scanning = True
        self.scan_btn.config(text="⏹ 停止")
        hosts = list(network.hosts())
        self.progress_var.set(0)
        threading.Thread(target=self._scan_range,
                         args=(hosts,), daemon=True).start()

    def _scan_range(self, hosts):
        total = len(hosts)
        found = 0
        semaphore = threading.Semaphore(self.threads_var.get())
        results = []
        lock = threading.Lock()

        def scan_host(ip):
            nonlocal found
            with semaphore:
                if not self.scanning:
                    return
                ip_str = str(ip)
                online, ms = self._ping(ip_str)
                if online:
                    hostname = self._get_hostname(ip_str)
                    ports = self._scan_ports(ip_str) if self.port_var.get() else ""
                    with lock:
                        found += 1
                        results.append((ip_str, hostname, ms, ports))
                        self.root.after(0, self._add_result,
                                        ip_str, hostname, ms, ports)

        threads = []
        for i, ip in enumerate(hosts):
            if not self.scanning:
                break
            t = threading.Thread(target=scan_host, args=(ip,), daemon=True)
            t.start()
            threads.append(t)
            pct = int((i + 1) / total * 100)
            self.root.after(0, self.progress_var.set, pct)
            self.root.after(0, self.status_var.set,
                            f"スキャン中: {i+1}/{total}  発見: {found}")

        for t in threads:
            t.join(timeout=2)

        self.root.after(0, self._scan_done, found)

    def _ping(self, ip):
        """pingを実行して(alive, ms)を返す"""
        timeout_s = self.timeout_var.get() / 1000
        try:
            param = "-n" if platform.system().lower() == "windows" else "-c"
            cmd = ["ping", param, "1", "-w" if platform.system().lower() == "windows"
                   else "-W", str(self.timeout_var.get()), ip]
            import time
            t0 = time.time()
            result = subprocess.run(cmd, capture_output=True,
                                     timeout=timeout_s + 1)
            ms = int((time.time() - t0) * 1000)
            alive = result.returncode == 0
            return alive, ms
        except Exception:
            # socketで接続チェック
            try:
                import time
                t0 = time.time()
                s = socket.create_connection((ip, 80),
                                              timeout=timeout_s)
                s.close()
                ms = int((time.time() - t0) * 1000)
                return True, ms
            except Exception:
                return False, 0

    def _get_hostname(self, ip):
        try:
            return socket.gethostbyaddr(ip)[0]
        except Exception:
            return ""

    def _scan_ports(self, ip):
        common_ports = [21, 22, 23, 25, 53, 80, 110, 143, 443,
                        445, 3306, 3389, 5432, 8080, 8443]
        open_ports = []
        for port in common_ports:
            try:
                s = socket.create_connection((ip, port), timeout=0.3)
                s.close()
                open_ports.append(port)
            except Exception:
                pass
        return ", ".join(str(p) for p in open_ports)

    def _add_result(self, ip, hostname, ms, ports):
        self.tree.insert("", "end",
                         values=(ip, hostname, f"{ms} ms", ports, "オンライン"),
                         tags=("online",))

    def _scan_done(self, found):
        self.scanning = False
        self.scan_btn.config(text="▶ スキャン開始")
        self.progress_var.set(100)
        self.status_var.set(
            f"スキャン完了  {found} 台発見  "
            f"({datetime.now().strftime('%H:%M:%S')})")

    def _ping_single(self):
        ip = self.tool_ip_var.get().strip()
        if not ip:
            return
        threading.Thread(target=self._do_ping_single,
                         args=(ip,), daemon=True).start()

    def _do_ping_single(self, ip):
        alive, ms = self._ping(ip)
        msg = f"{ip}: {'応答あり' if alive else '応答なし'} ({ms} ms)"
        self.root.after(0, lambda: messagebox.showinfo("Ping結果", msg))

    def _resolve_host(self):
        target = self.tool_ip_var.get().strip()
        if not target:
            return
        try:
            result = socket.gethostbyaddr(target)
            messagebox.showinfo("ホスト名解決",
                                f"ホスト名: {result[0]}\nエイリアス: {result[1]}\nIP: {result[2]}")
        except Exception as e:
            messagebox.showinfo("ホスト名解決", f"解決できませんでした: {e}")

    def _traceroute(self):
        ip = self.tool_ip_var.get().strip()
        if not ip:
            return
        win = tk.Toplevel(self.root)
        win.title(f"Traceroute — {ip}")
        win.geometry("500x300")
        txt = tk.Text(win, font=("Courier New", 10), bg="#1e1e1e",
                      fg="#ccc")
        txt.pack(fill=tk.BOTH, expand=True)

        def run():
            cmd = (["tracert", ip] if platform.system().lower() == "windows"
                   else ["traceroute", ip])
            try:
                proc = subprocess.Popen(cmd, stdout=subprocess.PIPE,
                                         stderr=subprocess.STDOUT,
                                         text=True)
                for line in proc.stdout:
                    win.after(0, txt.insert, tk.END, line)
            except Exception as e:
                win.after(0, txt.insert, tk.END, f"エラー: {e}")

        threading.Thread(target=run, daemon=True).start()


if __name__ == "__main__":
    root = tk.Tk()
    app = App13(root)
    root.mainloop()

LabelFrameによるセクション分け

ttk.LabelFrame を使うことで、入力エリアと結果エリアを視覚的に分けられます。padding引数でフレーム内の余白を設定し、見やすいレイアウトを実現しています。

import tkinter as tk
from tkinter import ttk, messagebox
import socket
import subprocess
import threading
import ipaddress
import platform
import re
from datetime import datetime


class App13:
    """ネットワークスキャナー"""

    def __init__(self, root):
        self.root = root
        self.root.title("ネットワークスキャナー")
        self.root.geometry("820x580")
        self.root.configure(bg="#f8f9fc")
        self.scanning = False
        self._build_ui()
        self._detect_local_ip()

    def _build_ui(self):
        title_frame = tk.Frame(self.root, bg="#e65100", pady=10)
        title_frame.pack(fill=tk.X)
        tk.Label(title_frame, text="🔍 ネットワークスキャナー",
                 font=("Noto Sans JP", 15, "bold"),
                 bg="#e65100", fg="white").pack(side=tk.LEFT, padx=12)

        # スキャン設定
        cfg_frame = ttk.LabelFrame(self.root, text="スキャン設定", padding=10)
        cfg_frame.pack(fill=tk.X, padx=8, pady=6)

        tk.Label(cfg_frame, text="IPレンジ:").grid(row=0, column=0, sticky="w")
        self.range_var = tk.StringVar(value="192.168.1.0/24")
        ttk.Entry(cfg_frame, textvariable=self.range_var,
                  width=22, font=("Courier New", 11)).grid(
            row=0, column=1, padx=8, sticky="w")

        tk.Label(cfg_frame, text="タイムアウト(ms):").grid(
            row=0, column=2, sticky="w", padx=(16, 4))
        self.timeout_var = tk.IntVar(value=300)
        ttk.Spinbox(cfg_frame, from_=50, to=2000,
                    textvariable=self.timeout_var, width=6).grid(row=0, column=3)

        tk.Label(cfg_frame, text="スレッド数:").grid(
            row=0, column=4, sticky="w", padx=(16, 4))
        self.threads_var = tk.IntVar(value=50)
        ttk.Spinbox(cfg_frame, from_=1, to=200,
                    textvariable=self.threads_var, width=6).grid(row=0, column=5)

        self.port_var = tk.BooleanVar(value=False)
        ttk.Checkbutton(cfg_frame, text="ポートスキャン",
                        variable=self.port_var).grid(row=0, column=6, padx=16)

        self.scan_btn = ttk.Button(cfg_frame, text="▶ スキャン開始",
                                   command=self._toggle_scan)
        self.scan_btn.grid(row=0, column=7, padx=8)

        # プログレス
        self.progress_var = tk.IntVar(value=0)
        self.progress = ttk.Progressbar(self.root, variable=self.progress_var,
                                         maximum=100)
        self.progress.pack(fill=tk.X, padx=8)

        # 結果テーブル
        result_frame = ttk.LabelFrame(self.root, text="スキャン結果", padding=4)
        result_frame.pack(fill=tk.BOTH, expand=True, padx=8, pady=6)
        cols = ("ip", "hostname", "ping", "ports", "status")
        self.tree = ttk.Treeview(result_frame, columns=cols,
                                  show="headings", selectmode="browse")
        for c, h, w in [("ip", "IPアドレス", 130), ("hostname", "ホスト名", 200),
                         ("ping", "応答(ms)", 80), ("ports", "開放ポート", 200),
                         ("status", "状態", 70)]:
            self.tree.heading(c, text=h)
            self.tree.column(c, width=w, minwidth=40)
        v_sb = ttk.Scrollbar(result_frame, command=self.tree.yview)
        self.tree.configure(yscrollcommand=v_sb.set)
        v_sb.pack(side=tk.RIGHT, fill=tk.Y)
        self.tree.pack(fill=tk.BOTH, expand=True)
        self.tree.tag_configure("online", foreground="#27ae60")
        self.tree.tag_configure("offline", foreground="#bbb")

        # 下部: ローカル情報 + ツール
        bottom = tk.Frame(self.root, bg="#f8f9fc")
        bottom.pack(fill=tk.X, padx=8, pady=4)
        self.local_info_var = tk.StringVar(value="")
        tk.Label(bottom, textvariable=self.local_info_var,
                 bg="#f8f9fc", font=("Courier New", 10),
                 fg="#555").pack(side=tk.LEFT)

        tool_frame = tk.Frame(bottom, bg="#f8f9fc")
        tool_frame.pack(side=tk.RIGHT)
        tk.Label(tool_frame, text="ツール: ", bg="#f8f9fc").pack(side=tk.LEFT)
        self.tool_ip_var = tk.StringVar()
        ttk.Entry(tool_frame, textvariable=self.tool_ip_var, width=16).pack(side=tk.LEFT, padx=4)
        for text, cmd in [("Ping", self._ping_single),
                           ("ホスト名解決", self._resolve_host),
                           ("Traceroute", self._traceroute)]:
            ttk.Button(tool_frame, text=text, command=cmd).pack(side=tk.LEFT, padx=2)

        self.status_var = tk.StringVar(value="スキャン待機中")
        tk.Label(self.root, textvariable=self.status_var,
                 bg="#dde", font=("Arial", 9), anchor="w", padx=8
                 ).pack(fill=tk.X, side=tk.BOTTOM)

    def _detect_local_ip(self):
        try:
            s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
            s.connect(("8.8.8.8", 80))
            local_ip = s.getsockname()[0]
            s.close()
            # デフォルトレンジをセット
            parts = local_ip.rsplit(".", 1)
            self.range_var.set(f"{parts[0]}.0/24")
            hostname = socket.gethostname()
            self.local_info_var.set(
                f"ホスト: {hostname}  ローカルIP: {local_ip}")
            self.tool_ip_var.set(local_ip)
        except Exception:
            pass

    def _toggle_scan(self):
        if self.scanning:
            self.scanning = False
            self.scan_btn.config(text="▶ スキャン開始")
        else:
            self._start_scan()

    def _start_scan(self):
        try:
            network = ipaddress.ip_network(self.range_var.get().strip(),
                                           strict=False)
        except ValueError as e:
            messagebox.showerror("エラー", f"無効なIPレンジ: {e}")
            return
        self.tree.delete(*self.tree.get_children())
        self.scanning = True
        self.scan_btn.config(text="⏹ 停止")
        hosts = list(network.hosts())
        self.progress_var.set(0)
        threading.Thread(target=self._scan_range,
                         args=(hosts,), daemon=True).start()

    def _scan_range(self, hosts):
        total = len(hosts)
        found = 0
        semaphore = threading.Semaphore(self.threads_var.get())
        results = []
        lock = threading.Lock()

        def scan_host(ip):
            nonlocal found
            with semaphore:
                if not self.scanning:
                    return
                ip_str = str(ip)
                online, ms = self._ping(ip_str)
                if online:
                    hostname = self._get_hostname(ip_str)
                    ports = self._scan_ports(ip_str) if self.port_var.get() else ""
                    with lock:
                        found += 1
                        results.append((ip_str, hostname, ms, ports))
                        self.root.after(0, self._add_result,
                                        ip_str, hostname, ms, ports)

        threads = []
        for i, ip in enumerate(hosts):
            if not self.scanning:
                break
            t = threading.Thread(target=scan_host, args=(ip,), daemon=True)
            t.start()
            threads.append(t)
            pct = int((i + 1) / total * 100)
            self.root.after(0, self.progress_var.set, pct)
            self.root.after(0, self.status_var.set,
                            f"スキャン中: {i+1}/{total}  発見: {found}")

        for t in threads:
            t.join(timeout=2)

        self.root.after(0, self._scan_done, found)

    def _ping(self, ip):
        """pingを実行して(alive, ms)を返す"""
        timeout_s = self.timeout_var.get() / 1000
        try:
            param = "-n" if platform.system().lower() == "windows" else "-c"
            cmd = ["ping", param, "1", "-w" if platform.system().lower() == "windows"
                   else "-W", str(self.timeout_var.get()), ip]
            import time
            t0 = time.time()
            result = subprocess.run(cmd, capture_output=True,
                                     timeout=timeout_s + 1)
            ms = int((time.time() - t0) * 1000)
            alive = result.returncode == 0
            return alive, ms
        except Exception:
            # socketで接続チェック
            try:
                import time
                t0 = time.time()
                s = socket.create_connection((ip, 80),
                                              timeout=timeout_s)
                s.close()
                ms = int((time.time() - t0) * 1000)
                return True, ms
            except Exception:
                return False, 0

    def _get_hostname(self, ip):
        try:
            return socket.gethostbyaddr(ip)[0]
        except Exception:
            return ""

    def _scan_ports(self, ip):
        common_ports = [21, 22, 23, 25, 53, 80, 110, 143, 443,
                        445, 3306, 3389, 5432, 8080, 8443]
        open_ports = []
        for port in common_ports:
            try:
                s = socket.create_connection((ip, port), timeout=0.3)
                s.close()
                open_ports.append(port)
            except Exception:
                pass
        return ", ".join(str(p) for p in open_ports)

    def _add_result(self, ip, hostname, ms, ports):
        self.tree.insert("", "end",
                         values=(ip, hostname, f"{ms} ms", ports, "オンライン"),
                         tags=("online",))

    def _scan_done(self, found):
        self.scanning = False
        self.scan_btn.config(text="▶ スキャン開始")
        self.progress_var.set(100)
        self.status_var.set(
            f"スキャン完了  {found} 台発見  "
            f"({datetime.now().strftime('%H:%M:%S')})")

    def _ping_single(self):
        ip = self.tool_ip_var.get().strip()
        if not ip:
            return
        threading.Thread(target=self._do_ping_single,
                         args=(ip,), daemon=True).start()

    def _do_ping_single(self, ip):
        alive, ms = self._ping(ip)
        msg = f"{ip}: {'応答あり' if alive else '応答なし'} ({ms} ms)"
        self.root.after(0, lambda: messagebox.showinfo("Ping結果", msg))

    def _resolve_host(self):
        target = self.tool_ip_var.get().strip()
        if not target:
            return
        try:
            result = socket.gethostbyaddr(target)
            messagebox.showinfo("ホスト名解決",
                                f"ホスト名: {result[0]}\nエイリアス: {result[1]}\nIP: {result[2]}")
        except Exception as e:
            messagebox.showinfo("ホスト名解決", f"解決できませんでした: {e}")

    def _traceroute(self):
        ip = self.tool_ip_var.get().strip()
        if not ip:
            return
        win = tk.Toplevel(self.root)
        win.title(f"Traceroute — {ip}")
        win.geometry("500x300")
        txt = tk.Text(win, font=("Courier New", 10), bg="#1e1e1e",
                      fg="#ccc")
        txt.pack(fill=tk.BOTH, expand=True)

        def run():
            cmd = (["tracert", ip] if platform.system().lower() == "windows"
                   else ["traceroute", ip])
            try:
                proc = subprocess.Popen(cmd, stdout=subprocess.PIPE,
                                         stderr=subprocess.STDOUT,
                                         text=True)
                for line in proc.stdout:
                    win.after(0, txt.insert, tk.END, line)
            except Exception as e:
                win.after(0, txt.insert, tk.END, f"エラー: {e}")

        threading.Thread(target=run, daemon=True).start()


if __name__ == "__main__":
    root = tk.Tk()
    app = App13(root)
    root.mainloop()

Entryウィジェットとイベントバインド

ttk.Entryで入力フィールドを作成します。bind('', ...)でEnterキー押下時に処理を実行できます。これにより、マウスを使わずキーボードだけで操作できるUXが実現できます。

import tkinter as tk
from tkinter import ttk, messagebox
import socket
import subprocess
import threading
import ipaddress
import platform
import re
from datetime import datetime


class App13:
    """ネットワークスキャナー"""

    def __init__(self, root):
        self.root = root
        self.root.title("ネットワークスキャナー")
        self.root.geometry("820x580")
        self.root.configure(bg="#f8f9fc")
        self.scanning = False
        self._build_ui()
        self._detect_local_ip()

    def _build_ui(self):
        title_frame = tk.Frame(self.root, bg="#e65100", pady=10)
        title_frame.pack(fill=tk.X)
        tk.Label(title_frame, text="🔍 ネットワークスキャナー",
                 font=("Noto Sans JP", 15, "bold"),
                 bg="#e65100", fg="white").pack(side=tk.LEFT, padx=12)

        # スキャン設定
        cfg_frame = ttk.LabelFrame(self.root, text="スキャン設定", padding=10)
        cfg_frame.pack(fill=tk.X, padx=8, pady=6)

        tk.Label(cfg_frame, text="IPレンジ:").grid(row=0, column=0, sticky="w")
        self.range_var = tk.StringVar(value="192.168.1.0/24")
        ttk.Entry(cfg_frame, textvariable=self.range_var,
                  width=22, font=("Courier New", 11)).grid(
            row=0, column=1, padx=8, sticky="w")

        tk.Label(cfg_frame, text="タイムアウト(ms):").grid(
            row=0, column=2, sticky="w", padx=(16, 4))
        self.timeout_var = tk.IntVar(value=300)
        ttk.Spinbox(cfg_frame, from_=50, to=2000,
                    textvariable=self.timeout_var, width=6).grid(row=0, column=3)

        tk.Label(cfg_frame, text="スレッド数:").grid(
            row=0, column=4, sticky="w", padx=(16, 4))
        self.threads_var = tk.IntVar(value=50)
        ttk.Spinbox(cfg_frame, from_=1, to=200,
                    textvariable=self.threads_var, width=6).grid(row=0, column=5)

        self.port_var = tk.BooleanVar(value=False)
        ttk.Checkbutton(cfg_frame, text="ポートスキャン",
                        variable=self.port_var).grid(row=0, column=6, padx=16)

        self.scan_btn = ttk.Button(cfg_frame, text="▶ スキャン開始",
                                   command=self._toggle_scan)
        self.scan_btn.grid(row=0, column=7, padx=8)

        # プログレス
        self.progress_var = tk.IntVar(value=0)
        self.progress = ttk.Progressbar(self.root, variable=self.progress_var,
                                         maximum=100)
        self.progress.pack(fill=tk.X, padx=8)

        # 結果テーブル
        result_frame = ttk.LabelFrame(self.root, text="スキャン結果", padding=4)
        result_frame.pack(fill=tk.BOTH, expand=True, padx=8, pady=6)
        cols = ("ip", "hostname", "ping", "ports", "status")
        self.tree = ttk.Treeview(result_frame, columns=cols,
                                  show="headings", selectmode="browse")
        for c, h, w in [("ip", "IPアドレス", 130), ("hostname", "ホスト名", 200),
                         ("ping", "応答(ms)", 80), ("ports", "開放ポート", 200),
                         ("status", "状態", 70)]:
            self.tree.heading(c, text=h)
            self.tree.column(c, width=w, minwidth=40)
        v_sb = ttk.Scrollbar(result_frame, command=self.tree.yview)
        self.tree.configure(yscrollcommand=v_sb.set)
        v_sb.pack(side=tk.RIGHT, fill=tk.Y)
        self.tree.pack(fill=tk.BOTH, expand=True)
        self.tree.tag_configure("online", foreground="#27ae60")
        self.tree.tag_configure("offline", foreground="#bbb")

        # 下部: ローカル情報 + ツール
        bottom = tk.Frame(self.root, bg="#f8f9fc")
        bottom.pack(fill=tk.X, padx=8, pady=4)
        self.local_info_var = tk.StringVar(value="")
        tk.Label(bottom, textvariable=self.local_info_var,
                 bg="#f8f9fc", font=("Courier New", 10),
                 fg="#555").pack(side=tk.LEFT)

        tool_frame = tk.Frame(bottom, bg="#f8f9fc")
        tool_frame.pack(side=tk.RIGHT)
        tk.Label(tool_frame, text="ツール: ", bg="#f8f9fc").pack(side=tk.LEFT)
        self.tool_ip_var = tk.StringVar()
        ttk.Entry(tool_frame, textvariable=self.tool_ip_var, width=16).pack(side=tk.LEFT, padx=4)
        for text, cmd in [("Ping", self._ping_single),
                           ("ホスト名解決", self._resolve_host),
                           ("Traceroute", self._traceroute)]:
            ttk.Button(tool_frame, text=text, command=cmd).pack(side=tk.LEFT, padx=2)

        self.status_var = tk.StringVar(value="スキャン待機中")
        tk.Label(self.root, textvariable=self.status_var,
                 bg="#dde", font=("Arial", 9), anchor="w", padx=8
                 ).pack(fill=tk.X, side=tk.BOTTOM)

    def _detect_local_ip(self):
        try:
            s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
            s.connect(("8.8.8.8", 80))
            local_ip = s.getsockname()[0]
            s.close()
            # デフォルトレンジをセット
            parts = local_ip.rsplit(".", 1)
            self.range_var.set(f"{parts[0]}.0/24")
            hostname = socket.gethostname()
            self.local_info_var.set(
                f"ホスト: {hostname}  ローカルIP: {local_ip}")
            self.tool_ip_var.set(local_ip)
        except Exception:
            pass

    def _toggle_scan(self):
        if self.scanning:
            self.scanning = False
            self.scan_btn.config(text="▶ スキャン開始")
        else:
            self._start_scan()

    def _start_scan(self):
        try:
            network = ipaddress.ip_network(self.range_var.get().strip(),
                                           strict=False)
        except ValueError as e:
            messagebox.showerror("エラー", f"無効なIPレンジ: {e}")
            return
        self.tree.delete(*self.tree.get_children())
        self.scanning = True
        self.scan_btn.config(text="⏹ 停止")
        hosts = list(network.hosts())
        self.progress_var.set(0)
        threading.Thread(target=self._scan_range,
                         args=(hosts,), daemon=True).start()

    def _scan_range(self, hosts):
        total = len(hosts)
        found = 0
        semaphore = threading.Semaphore(self.threads_var.get())
        results = []
        lock = threading.Lock()

        def scan_host(ip):
            nonlocal found
            with semaphore:
                if not self.scanning:
                    return
                ip_str = str(ip)
                online, ms = self._ping(ip_str)
                if online:
                    hostname = self._get_hostname(ip_str)
                    ports = self._scan_ports(ip_str) if self.port_var.get() else ""
                    with lock:
                        found += 1
                        results.append((ip_str, hostname, ms, ports))
                        self.root.after(0, self._add_result,
                                        ip_str, hostname, ms, ports)

        threads = []
        for i, ip in enumerate(hosts):
            if not self.scanning:
                break
            t = threading.Thread(target=scan_host, args=(ip,), daemon=True)
            t.start()
            threads.append(t)
            pct = int((i + 1) / total * 100)
            self.root.after(0, self.progress_var.set, pct)
            self.root.after(0, self.status_var.set,
                            f"スキャン中: {i+1}/{total}  発見: {found}")

        for t in threads:
            t.join(timeout=2)

        self.root.after(0, self._scan_done, found)

    def _ping(self, ip):
        """pingを実行して(alive, ms)を返す"""
        timeout_s = self.timeout_var.get() / 1000
        try:
            param = "-n" if platform.system().lower() == "windows" else "-c"
            cmd = ["ping", param, "1", "-w" if platform.system().lower() == "windows"
                   else "-W", str(self.timeout_var.get()), ip]
            import time
            t0 = time.time()
            result = subprocess.run(cmd, capture_output=True,
                                     timeout=timeout_s + 1)
            ms = int((time.time() - t0) * 1000)
            alive = result.returncode == 0
            return alive, ms
        except Exception:
            # socketで接続チェック
            try:
                import time
                t0 = time.time()
                s = socket.create_connection((ip, 80),
                                              timeout=timeout_s)
                s.close()
                ms = int((time.time() - t0) * 1000)
                return True, ms
            except Exception:
                return False, 0

    def _get_hostname(self, ip):
        try:
            return socket.gethostbyaddr(ip)[0]
        except Exception:
            return ""

    def _scan_ports(self, ip):
        common_ports = [21, 22, 23, 25, 53, 80, 110, 143, 443,
                        445, 3306, 3389, 5432, 8080, 8443]
        open_ports = []
        for port in common_ports:
            try:
                s = socket.create_connection((ip, port), timeout=0.3)
                s.close()
                open_ports.append(port)
            except Exception:
                pass
        return ", ".join(str(p) for p in open_ports)

    def _add_result(self, ip, hostname, ms, ports):
        self.tree.insert("", "end",
                         values=(ip, hostname, f"{ms} ms", ports, "オンライン"),
                         tags=("online",))

    def _scan_done(self, found):
        self.scanning = False
        self.scan_btn.config(text="▶ スキャン開始")
        self.progress_var.set(100)
        self.status_var.set(
            f"スキャン完了  {found} 台発見  "
            f"({datetime.now().strftime('%H:%M:%S')})")

    def _ping_single(self):
        ip = self.tool_ip_var.get().strip()
        if not ip:
            return
        threading.Thread(target=self._do_ping_single,
                         args=(ip,), daemon=True).start()

    def _do_ping_single(self, ip):
        alive, ms = self._ping(ip)
        msg = f"{ip}: {'応答あり' if alive else '応答なし'} ({ms} ms)"
        self.root.after(0, lambda: messagebox.showinfo("Ping結果", msg))

    def _resolve_host(self):
        target = self.tool_ip_var.get().strip()
        if not target:
            return
        try:
            result = socket.gethostbyaddr(target)
            messagebox.showinfo("ホスト名解決",
                                f"ホスト名: {result[0]}\nエイリアス: {result[1]}\nIP: {result[2]}")
        except Exception as e:
            messagebox.showinfo("ホスト名解決", f"解決できませんでした: {e}")

    def _traceroute(self):
        ip = self.tool_ip_var.get().strip()
        if not ip:
            return
        win = tk.Toplevel(self.root)
        win.title(f"Traceroute — {ip}")
        win.geometry("500x300")
        txt = tk.Text(win, font=("Courier New", 10), bg="#1e1e1e",
                      fg="#ccc")
        txt.pack(fill=tk.BOTH, expand=True)

        def run():
            cmd = (["tracert", ip] if platform.system().lower() == "windows"
                   else ["traceroute", ip])
            try:
                proc = subprocess.Popen(cmd, stdout=subprocess.PIPE,
                                         stderr=subprocess.STDOUT,
                                         text=True)
                for line in proc.stdout:
                    win.after(0, txt.insert, tk.END, line)
            except Exception as e:
                win.after(0, txt.insert, tk.END, f"エラー: {e}")

        threading.Thread(target=run, daemon=True).start()


if __name__ == "__main__":
    root = tk.Tk()
    app = App13(root)
    root.mainloop()

Textウィジェットでの結果表示

結果表示にはtk.Textウィジェットを使います。state=tk.DISABLEDでユーザーが直接編集できないようにし、表示前にNORMALに切り替えてからinsert()で内容を更新します。

import tkinter as tk
from tkinter import ttk, messagebox
import socket
import subprocess
import threading
import ipaddress
import platform
import re
from datetime import datetime


class App13:
    """ネットワークスキャナー"""

    def __init__(self, root):
        self.root = root
        self.root.title("ネットワークスキャナー")
        self.root.geometry("820x580")
        self.root.configure(bg="#f8f9fc")
        self.scanning = False
        self._build_ui()
        self._detect_local_ip()

    def _build_ui(self):
        title_frame = tk.Frame(self.root, bg="#e65100", pady=10)
        title_frame.pack(fill=tk.X)
        tk.Label(title_frame, text="🔍 ネットワークスキャナー",
                 font=("Noto Sans JP", 15, "bold"),
                 bg="#e65100", fg="white").pack(side=tk.LEFT, padx=12)

        # スキャン設定
        cfg_frame = ttk.LabelFrame(self.root, text="スキャン設定", padding=10)
        cfg_frame.pack(fill=tk.X, padx=8, pady=6)

        tk.Label(cfg_frame, text="IPレンジ:").grid(row=0, column=0, sticky="w")
        self.range_var = tk.StringVar(value="192.168.1.0/24")
        ttk.Entry(cfg_frame, textvariable=self.range_var,
                  width=22, font=("Courier New", 11)).grid(
            row=0, column=1, padx=8, sticky="w")

        tk.Label(cfg_frame, text="タイムアウト(ms):").grid(
            row=0, column=2, sticky="w", padx=(16, 4))
        self.timeout_var = tk.IntVar(value=300)
        ttk.Spinbox(cfg_frame, from_=50, to=2000,
                    textvariable=self.timeout_var, width=6).grid(row=0, column=3)

        tk.Label(cfg_frame, text="スレッド数:").grid(
            row=0, column=4, sticky="w", padx=(16, 4))
        self.threads_var = tk.IntVar(value=50)
        ttk.Spinbox(cfg_frame, from_=1, to=200,
                    textvariable=self.threads_var, width=6).grid(row=0, column=5)

        self.port_var = tk.BooleanVar(value=False)
        ttk.Checkbutton(cfg_frame, text="ポートスキャン",
                        variable=self.port_var).grid(row=0, column=6, padx=16)

        self.scan_btn = ttk.Button(cfg_frame, text="▶ スキャン開始",
                                   command=self._toggle_scan)
        self.scan_btn.grid(row=0, column=7, padx=8)

        # プログレス
        self.progress_var = tk.IntVar(value=0)
        self.progress = ttk.Progressbar(self.root, variable=self.progress_var,
                                         maximum=100)
        self.progress.pack(fill=tk.X, padx=8)

        # 結果テーブル
        result_frame = ttk.LabelFrame(self.root, text="スキャン結果", padding=4)
        result_frame.pack(fill=tk.BOTH, expand=True, padx=8, pady=6)
        cols = ("ip", "hostname", "ping", "ports", "status")
        self.tree = ttk.Treeview(result_frame, columns=cols,
                                  show="headings", selectmode="browse")
        for c, h, w in [("ip", "IPアドレス", 130), ("hostname", "ホスト名", 200),
                         ("ping", "応答(ms)", 80), ("ports", "開放ポート", 200),
                         ("status", "状態", 70)]:
            self.tree.heading(c, text=h)
            self.tree.column(c, width=w, minwidth=40)
        v_sb = ttk.Scrollbar(result_frame, command=self.tree.yview)
        self.tree.configure(yscrollcommand=v_sb.set)
        v_sb.pack(side=tk.RIGHT, fill=tk.Y)
        self.tree.pack(fill=tk.BOTH, expand=True)
        self.tree.tag_configure("online", foreground="#27ae60")
        self.tree.tag_configure("offline", foreground="#bbb")

        # 下部: ローカル情報 + ツール
        bottom = tk.Frame(self.root, bg="#f8f9fc")
        bottom.pack(fill=tk.X, padx=8, pady=4)
        self.local_info_var = tk.StringVar(value="")
        tk.Label(bottom, textvariable=self.local_info_var,
                 bg="#f8f9fc", font=("Courier New", 10),
                 fg="#555").pack(side=tk.LEFT)

        tool_frame = tk.Frame(bottom, bg="#f8f9fc")
        tool_frame.pack(side=tk.RIGHT)
        tk.Label(tool_frame, text="ツール: ", bg="#f8f9fc").pack(side=tk.LEFT)
        self.tool_ip_var = tk.StringVar()
        ttk.Entry(tool_frame, textvariable=self.tool_ip_var, width=16).pack(side=tk.LEFT, padx=4)
        for text, cmd in [("Ping", self._ping_single),
                           ("ホスト名解決", self._resolve_host),
                           ("Traceroute", self._traceroute)]:
            ttk.Button(tool_frame, text=text, command=cmd).pack(side=tk.LEFT, padx=2)

        self.status_var = tk.StringVar(value="スキャン待機中")
        tk.Label(self.root, textvariable=self.status_var,
                 bg="#dde", font=("Arial", 9), anchor="w", padx=8
                 ).pack(fill=tk.X, side=tk.BOTTOM)

    def _detect_local_ip(self):
        try:
            s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
            s.connect(("8.8.8.8", 80))
            local_ip = s.getsockname()[0]
            s.close()
            # デフォルトレンジをセット
            parts = local_ip.rsplit(".", 1)
            self.range_var.set(f"{parts[0]}.0/24")
            hostname = socket.gethostname()
            self.local_info_var.set(
                f"ホスト: {hostname}  ローカルIP: {local_ip}")
            self.tool_ip_var.set(local_ip)
        except Exception:
            pass

    def _toggle_scan(self):
        if self.scanning:
            self.scanning = False
            self.scan_btn.config(text="▶ スキャン開始")
        else:
            self._start_scan()

    def _start_scan(self):
        try:
            network = ipaddress.ip_network(self.range_var.get().strip(),
                                           strict=False)
        except ValueError as e:
            messagebox.showerror("エラー", f"無効なIPレンジ: {e}")
            return
        self.tree.delete(*self.tree.get_children())
        self.scanning = True
        self.scan_btn.config(text="⏹ 停止")
        hosts = list(network.hosts())
        self.progress_var.set(0)
        threading.Thread(target=self._scan_range,
                         args=(hosts,), daemon=True).start()

    def _scan_range(self, hosts):
        total = len(hosts)
        found = 0
        semaphore = threading.Semaphore(self.threads_var.get())
        results = []
        lock = threading.Lock()

        def scan_host(ip):
            nonlocal found
            with semaphore:
                if not self.scanning:
                    return
                ip_str = str(ip)
                online, ms = self._ping(ip_str)
                if online:
                    hostname = self._get_hostname(ip_str)
                    ports = self._scan_ports(ip_str) if self.port_var.get() else ""
                    with lock:
                        found += 1
                        results.append((ip_str, hostname, ms, ports))
                        self.root.after(0, self._add_result,
                                        ip_str, hostname, ms, ports)

        threads = []
        for i, ip in enumerate(hosts):
            if not self.scanning:
                break
            t = threading.Thread(target=scan_host, args=(ip,), daemon=True)
            t.start()
            threads.append(t)
            pct = int((i + 1) / total * 100)
            self.root.after(0, self.progress_var.set, pct)
            self.root.after(0, self.status_var.set,
                            f"スキャン中: {i+1}/{total}  発見: {found}")

        for t in threads:
            t.join(timeout=2)

        self.root.after(0, self._scan_done, found)

    def _ping(self, ip):
        """pingを実行して(alive, ms)を返す"""
        timeout_s = self.timeout_var.get() / 1000
        try:
            param = "-n" if platform.system().lower() == "windows" else "-c"
            cmd = ["ping", param, "1", "-w" if platform.system().lower() == "windows"
                   else "-W", str(self.timeout_var.get()), ip]
            import time
            t0 = time.time()
            result = subprocess.run(cmd, capture_output=True,
                                     timeout=timeout_s + 1)
            ms = int((time.time() - t0) * 1000)
            alive = result.returncode == 0
            return alive, ms
        except Exception:
            # socketで接続チェック
            try:
                import time
                t0 = time.time()
                s = socket.create_connection((ip, 80),
                                              timeout=timeout_s)
                s.close()
                ms = int((time.time() - t0) * 1000)
                return True, ms
            except Exception:
                return False, 0

    def _get_hostname(self, ip):
        try:
            return socket.gethostbyaddr(ip)[0]
        except Exception:
            return ""

    def _scan_ports(self, ip):
        common_ports = [21, 22, 23, 25, 53, 80, 110, 143, 443,
                        445, 3306, 3389, 5432, 8080, 8443]
        open_ports = []
        for port in common_ports:
            try:
                s = socket.create_connection((ip, port), timeout=0.3)
                s.close()
                open_ports.append(port)
            except Exception:
                pass
        return ", ".join(str(p) for p in open_ports)

    def _add_result(self, ip, hostname, ms, ports):
        self.tree.insert("", "end",
                         values=(ip, hostname, f"{ms} ms", ports, "オンライン"),
                         tags=("online",))

    def _scan_done(self, found):
        self.scanning = False
        self.scan_btn.config(text="▶ スキャン開始")
        self.progress_var.set(100)
        self.status_var.set(
            f"スキャン完了  {found} 台発見  "
            f"({datetime.now().strftime('%H:%M:%S')})")

    def _ping_single(self):
        ip = self.tool_ip_var.get().strip()
        if not ip:
            return
        threading.Thread(target=self._do_ping_single,
                         args=(ip,), daemon=True).start()

    def _do_ping_single(self, ip):
        alive, ms = self._ping(ip)
        msg = f"{ip}: {'応答あり' if alive else '応答なし'} ({ms} ms)"
        self.root.after(0, lambda: messagebox.showinfo("Ping結果", msg))

    def _resolve_host(self):
        target = self.tool_ip_var.get().strip()
        if not target:
            return
        try:
            result = socket.gethostbyaddr(target)
            messagebox.showinfo("ホスト名解決",
                                f"ホスト名: {result[0]}\nエイリアス: {result[1]}\nIP: {result[2]}")
        except Exception as e:
            messagebox.showinfo("ホスト名解決", f"解決できませんでした: {e}")

    def _traceroute(self):
        ip = self.tool_ip_var.get().strip()
        if not ip:
            return
        win = tk.Toplevel(self.root)
        win.title(f"Traceroute — {ip}")
        win.geometry("500x300")
        txt = tk.Text(win, font=("Courier New", 10), bg="#1e1e1e",
                      fg="#ccc")
        txt.pack(fill=tk.BOTH, expand=True)

        def run():
            cmd = (["tracert", ip] if platform.system().lower() == "windows"
                   else ["traceroute", ip])
            try:
                proc = subprocess.Popen(cmd, stdout=subprocess.PIPE,
                                         stderr=subprocess.STDOUT,
                                         text=True)
                for line in proc.stdout:
                    win.after(0, txt.insert, tk.END, line)
            except Exception as e:
                win.after(0, txt.insert, tk.END, f"エラー: {e}")

        threading.Thread(target=run, daemon=True).start()


if __name__ == "__main__":
    root = tk.Tk()
    app = App13(root)
    root.mainloop()

例外処理とmessagebox

try-except で ValueError と Exception を捕捉し、messagebox.showerror() でユーザーにわかりやすいエラーメッセージを表示します。入力バリデーションは必ず実装しましょう。

import tkinter as tk
from tkinter import ttk, messagebox
import socket
import subprocess
import threading
import ipaddress
import platform
import re
from datetime import datetime


class App13:
    """ネットワークスキャナー"""

    def __init__(self, root):
        self.root = root
        self.root.title("ネットワークスキャナー")
        self.root.geometry("820x580")
        self.root.configure(bg="#f8f9fc")
        self.scanning = False
        self._build_ui()
        self._detect_local_ip()

    def _build_ui(self):
        title_frame = tk.Frame(self.root, bg="#e65100", pady=10)
        title_frame.pack(fill=tk.X)
        tk.Label(title_frame, text="🔍 ネットワークスキャナー",
                 font=("Noto Sans JP", 15, "bold"),
                 bg="#e65100", fg="white").pack(side=tk.LEFT, padx=12)

        # スキャン設定
        cfg_frame = ttk.LabelFrame(self.root, text="スキャン設定", padding=10)
        cfg_frame.pack(fill=tk.X, padx=8, pady=6)

        tk.Label(cfg_frame, text="IPレンジ:").grid(row=0, column=0, sticky="w")
        self.range_var = tk.StringVar(value="192.168.1.0/24")
        ttk.Entry(cfg_frame, textvariable=self.range_var,
                  width=22, font=("Courier New", 11)).grid(
            row=0, column=1, padx=8, sticky="w")

        tk.Label(cfg_frame, text="タイムアウト(ms):").grid(
            row=0, column=2, sticky="w", padx=(16, 4))
        self.timeout_var = tk.IntVar(value=300)
        ttk.Spinbox(cfg_frame, from_=50, to=2000,
                    textvariable=self.timeout_var, width=6).grid(row=0, column=3)

        tk.Label(cfg_frame, text="スレッド数:").grid(
            row=0, column=4, sticky="w", padx=(16, 4))
        self.threads_var = tk.IntVar(value=50)
        ttk.Spinbox(cfg_frame, from_=1, to=200,
                    textvariable=self.threads_var, width=6).grid(row=0, column=5)

        self.port_var = tk.BooleanVar(value=False)
        ttk.Checkbutton(cfg_frame, text="ポートスキャン",
                        variable=self.port_var).grid(row=0, column=6, padx=16)

        self.scan_btn = ttk.Button(cfg_frame, text="▶ スキャン開始",
                                   command=self._toggle_scan)
        self.scan_btn.grid(row=0, column=7, padx=8)

        # プログレス
        self.progress_var = tk.IntVar(value=0)
        self.progress = ttk.Progressbar(self.root, variable=self.progress_var,
                                         maximum=100)
        self.progress.pack(fill=tk.X, padx=8)

        # 結果テーブル
        result_frame = ttk.LabelFrame(self.root, text="スキャン結果", padding=4)
        result_frame.pack(fill=tk.BOTH, expand=True, padx=8, pady=6)
        cols = ("ip", "hostname", "ping", "ports", "status")
        self.tree = ttk.Treeview(result_frame, columns=cols,
                                  show="headings", selectmode="browse")
        for c, h, w in [("ip", "IPアドレス", 130), ("hostname", "ホスト名", 200),
                         ("ping", "応答(ms)", 80), ("ports", "開放ポート", 200),
                         ("status", "状態", 70)]:
            self.tree.heading(c, text=h)
            self.tree.column(c, width=w, minwidth=40)
        v_sb = ttk.Scrollbar(result_frame, command=self.tree.yview)
        self.tree.configure(yscrollcommand=v_sb.set)
        v_sb.pack(side=tk.RIGHT, fill=tk.Y)
        self.tree.pack(fill=tk.BOTH, expand=True)
        self.tree.tag_configure("online", foreground="#27ae60")
        self.tree.tag_configure("offline", foreground="#bbb")

        # 下部: ローカル情報 + ツール
        bottom = tk.Frame(self.root, bg="#f8f9fc")
        bottom.pack(fill=tk.X, padx=8, pady=4)
        self.local_info_var = tk.StringVar(value="")
        tk.Label(bottom, textvariable=self.local_info_var,
                 bg="#f8f9fc", font=("Courier New", 10),
                 fg="#555").pack(side=tk.LEFT)

        tool_frame = tk.Frame(bottom, bg="#f8f9fc")
        tool_frame.pack(side=tk.RIGHT)
        tk.Label(tool_frame, text="ツール: ", bg="#f8f9fc").pack(side=tk.LEFT)
        self.tool_ip_var = tk.StringVar()
        ttk.Entry(tool_frame, textvariable=self.tool_ip_var, width=16).pack(side=tk.LEFT, padx=4)
        for text, cmd in [("Ping", self._ping_single),
                           ("ホスト名解決", self._resolve_host),
                           ("Traceroute", self._traceroute)]:
            ttk.Button(tool_frame, text=text, command=cmd).pack(side=tk.LEFT, padx=2)

        self.status_var = tk.StringVar(value="スキャン待機中")
        tk.Label(self.root, textvariable=self.status_var,
                 bg="#dde", font=("Arial", 9), anchor="w", padx=8
                 ).pack(fill=tk.X, side=tk.BOTTOM)

    def _detect_local_ip(self):
        try:
            s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
            s.connect(("8.8.8.8", 80))
            local_ip = s.getsockname()[0]
            s.close()
            # デフォルトレンジをセット
            parts = local_ip.rsplit(".", 1)
            self.range_var.set(f"{parts[0]}.0/24")
            hostname = socket.gethostname()
            self.local_info_var.set(
                f"ホスト: {hostname}  ローカルIP: {local_ip}")
            self.tool_ip_var.set(local_ip)
        except Exception:
            pass

    def _toggle_scan(self):
        if self.scanning:
            self.scanning = False
            self.scan_btn.config(text="▶ スキャン開始")
        else:
            self._start_scan()

    def _start_scan(self):
        try:
            network = ipaddress.ip_network(self.range_var.get().strip(),
                                           strict=False)
        except ValueError as e:
            messagebox.showerror("エラー", f"無効なIPレンジ: {e}")
            return
        self.tree.delete(*self.tree.get_children())
        self.scanning = True
        self.scan_btn.config(text="⏹ 停止")
        hosts = list(network.hosts())
        self.progress_var.set(0)
        threading.Thread(target=self._scan_range,
                         args=(hosts,), daemon=True).start()

    def _scan_range(self, hosts):
        total = len(hosts)
        found = 0
        semaphore = threading.Semaphore(self.threads_var.get())
        results = []
        lock = threading.Lock()

        def scan_host(ip):
            nonlocal found
            with semaphore:
                if not self.scanning:
                    return
                ip_str = str(ip)
                online, ms = self._ping(ip_str)
                if online:
                    hostname = self._get_hostname(ip_str)
                    ports = self._scan_ports(ip_str) if self.port_var.get() else ""
                    with lock:
                        found += 1
                        results.append((ip_str, hostname, ms, ports))
                        self.root.after(0, self._add_result,
                                        ip_str, hostname, ms, ports)

        threads = []
        for i, ip in enumerate(hosts):
            if not self.scanning:
                break
            t = threading.Thread(target=scan_host, args=(ip,), daemon=True)
            t.start()
            threads.append(t)
            pct = int((i + 1) / total * 100)
            self.root.after(0, self.progress_var.set, pct)
            self.root.after(0, self.status_var.set,
                            f"スキャン中: {i+1}/{total}  発見: {found}")

        for t in threads:
            t.join(timeout=2)

        self.root.after(0, self._scan_done, found)

    def _ping(self, ip):
        """pingを実行して(alive, ms)を返す"""
        timeout_s = self.timeout_var.get() / 1000
        try:
            param = "-n" if platform.system().lower() == "windows" else "-c"
            cmd = ["ping", param, "1", "-w" if platform.system().lower() == "windows"
                   else "-W", str(self.timeout_var.get()), ip]
            import time
            t0 = time.time()
            result = subprocess.run(cmd, capture_output=True,
                                     timeout=timeout_s + 1)
            ms = int((time.time() - t0) * 1000)
            alive = result.returncode == 0
            return alive, ms
        except Exception:
            # socketで接続チェック
            try:
                import time
                t0 = time.time()
                s = socket.create_connection((ip, 80),
                                              timeout=timeout_s)
                s.close()
                ms = int((time.time() - t0) * 1000)
                return True, ms
            except Exception:
                return False, 0

    def _get_hostname(self, ip):
        try:
            return socket.gethostbyaddr(ip)[0]
        except Exception:
            return ""

    def _scan_ports(self, ip):
        common_ports = [21, 22, 23, 25, 53, 80, 110, 143, 443,
                        445, 3306, 3389, 5432, 8080, 8443]
        open_ports = []
        for port in common_ports:
            try:
                s = socket.create_connection((ip, port), timeout=0.3)
                s.close()
                open_ports.append(port)
            except Exception:
                pass
        return ", ".join(str(p) for p in open_ports)

    def _add_result(self, ip, hostname, ms, ports):
        self.tree.insert("", "end",
                         values=(ip, hostname, f"{ms} ms", ports, "オンライン"),
                         tags=("online",))

    def _scan_done(self, found):
        self.scanning = False
        self.scan_btn.config(text="▶ スキャン開始")
        self.progress_var.set(100)
        self.status_var.set(
            f"スキャン完了  {found} 台発見  "
            f"({datetime.now().strftime('%H:%M:%S')})")

    def _ping_single(self):
        ip = self.tool_ip_var.get().strip()
        if not ip:
            return
        threading.Thread(target=self._do_ping_single,
                         args=(ip,), daemon=True).start()

    def _do_ping_single(self, ip):
        alive, ms = self._ping(ip)
        msg = f"{ip}: {'応答あり' if alive else '応答なし'} ({ms} ms)"
        self.root.after(0, lambda: messagebox.showinfo("Ping結果", msg))

    def _resolve_host(self):
        target = self.tool_ip_var.get().strip()
        if not target:
            return
        try:
            result = socket.gethostbyaddr(target)
            messagebox.showinfo("ホスト名解決",
                                f"ホスト名: {result[0]}\nエイリアス: {result[1]}\nIP: {result[2]}")
        except Exception as e:
            messagebox.showinfo("ホスト名解決", f"解決できませんでした: {e}")

    def _traceroute(self):
        ip = self.tool_ip_var.get().strip()
        if not ip:
            return
        win = tk.Toplevel(self.root)
        win.title(f"Traceroute — {ip}")
        win.geometry("500x300")
        txt = tk.Text(win, font=("Courier New", 10), bg="#1e1e1e",
                      fg="#ccc")
        txt.pack(fill=tk.BOTH, expand=True)

        def run():
            cmd = (["tracert", ip] if platform.system().lower() == "windows"
                   else ["traceroute", ip])
            try:
                proc = subprocess.Popen(cmd, stdout=subprocess.PIPE,
                                         stderr=subprocess.STDOUT,
                                         text=True)
                for line in proc.stdout:
                    win.after(0, txt.insert, tk.END, line)
            except Exception as e:
                win.after(0, txt.insert, tk.END, f"エラー: {e}")

        threading.Thread(target=run, daemon=True).start()


if __name__ == "__main__":
    root = tk.Tk()
    app = App13(root)
    root.mainloop()

6. ステップバイステップガイド

このアプリをゼロから自分で作る手順を解説します。コードをコピーするだけでなく、実際に手順を追って自分で書いてみましょう。

  1. 1
    ファイルを作成する

    新しいファイルを作成して app13.py と保存します。

  2. 2
    クラスの骨格を作る

    App13クラスを定義し、__init__とmainloop()の最小構成を作ります。

  3. 3
    タイトルバーを作る

    Frameを使ってカラーバー付きのタイトルエリアを作ります。

  4. 4
    入力フォームを実装する

    LabelFrameとEntryウィジェットで入力エリアを作ります。

  5. 5
    処理ロジックを実装する

    _calculate()メソッドに計算・処理ロジックを実装します。

  6. 6
    結果表示を実装する

    TextウィジェットかLabelに結果を表示する_show_result()を実装します。

  7. 7
    エラー処理を追加する

    try-exceptとmessageboxでエラーハンドリングを追加します。

7. カスタマイズアイデア

基本機能を習得したら、以下のカスタマイズに挑戦してみましょう。少しずつ機能を追加することで、Pythonのスキルが飛躍的に向上します。

💡 ダークモードを追加する

bg色・fg色を辞書で管理し、ボタン1つでダークモード・ライトモードを切り替えられるようにしましょう。

💡 データのエクスポート機能

計算結果をCSV・TXTファイルに保存するエクスポート機能を追加しましょう。filedialog.asksaveasfilename()でファイル保存ダイアログが使えます。

💡 入力履歴機能

以前の入力値を覚えておいてComboboxのドロップダウンで再選択できる履歴機能を追加しましょう。

8. よくある問題と解決法

❌ 日本語フォントが表示されない

原因:システムに日本語フォントが見つからない場合があります。

解決法:font引数を省略するかシステムに合ったフォントを指定してください。

❌ ウィンドウのサイズが変更できない

原因:resizable(False, False)が設定されています。

解決法:resizable(True, True)に変更してください。

9. 練習問題

アプリの理解を深めるための練習問題です。難易度順に挑戦してみてください。

  1. 課題1:機能拡張

    ネットワークスキャナーに新しい機能を1つ追加してみましょう。どんな機能があると便利か考えてから実装してください。

  2. 課題2:UIの改善

    色・フォント・レイアウトを変更して、より使いやすいUIにカスタマイズしてみましょう。

  3. 課題3:保存機能の追加

    入力値や計算結果をファイルに保存する機能を追加しましょう。jsonやcsvモジュールを使います。

🚀
次に挑戦するアプリ

このアプリをマスターしたら、次のNo.14に挑戦しましょう。