hoffstadt / hoffstadt/DearPyGui
[SHOWCASE] Made a GUI with animated values and bars
- Dominant language
- C++
- Stars
- 15.6k
- Forks
- 783
- PR merge metrics
- No merged PRs in 30d
Description
The GUI is used for managing aim settings for game Apex Legends.

source code
```py
import os
import re
import shutil
import subprocess
import time
from datetime import datetime
from pathlib import Path
from collections import OrderedDict
import dearpygui.dearpygui as dpg
# =========================
# Global constants (tweak here)
# =========================
ANIM_DURATION = 0.20 # seconds for bar/number tween (ease-out cubic)
BAR_COLOR = (235, 235, 235, 255) # off-white bar fill
WINDOW_W = 1180 # initial window width
WINDOW_H = 780 # initial window height
LABEL_PADDING_PX = 24 # extra px for label column after measuring
VALUE_COL_WIDTH = 120 # fixed width for numeric column
EXPORT_FLASH_SECS = 3.0 # how long to show "Copied profile!" after export
# ---------- Paths ----------
USERPROFILE = os.environ.get("USERPROFILE") or os.environ.get("HOME") or ""
APEX_DIR = Path(USERPROFILE) / "Saved Games" / "Respawn" / "Apex" / "profile"
ACTIVE_CFG = APEX_DIR / "profile.cfg"
# ---------- ALC definition (single source of truth, in-game order) ----------
ALC_STRUCTURE = OrderedDict([
("General", [
("gamepad_custom_deadzone_in", "Deadzone"),
("gamepad_custom_deadzone_out", "Outer Threshold"),
("gamepad_custom_curve", "Response Curve"),
]),
("Hip-fire", [
("gamepad_custom_hip_yaw", "Yaw Speed"),
("gamepad_custom_hip_pitch", "Pitch Speed"),
("gamepad_custom_hip_turn_yaw", "Turning Extra Yaw"),
("gamepad_custom_hip_turn_pitch", "Turning Extra Pitch"),
("gamepad_custom_hip_turn_time", "Turning Ramp-up Time"),
("gamepad_custom_hip_turn_delay", "Turning Ramp-up Delay"),
]),
("ADS", [
("gamepad_custom_ads_yaw", "ADS Yaw Speed"),
("gamepad_custom_ads_pitch", "ADS Pitch Speed"),
("gamepad_custom_ads_turn_yaw", "ADS Turning Extra Yaw"),
("gamepad_custom_ads_turn_pitch", "ADS Turning Extra Pitch"),
("gamepad_custom_ads_turn_time", "ADS Turning Ramp-up Time"),
("gamepad_custom_ads_turn_delay", "ADS Turning Ramp-up Delay"),
]),
("Per Optic ADS Sensitivity", [
("gamepad_ads_advanced_sensitivity_scalar_0", "1x Optic / Iron Sights"),
("gamepad_ads_advanced_sensitivity_scalar_1", "2x Optic"),
("gamepad_ads_advanced_sensitivity_scalar_2", "3x Optic"),
("gamepad_ads_advanced_sensitivity_scalar_3", "4x Optic"),
("gamepad_ads_advanced_sensitivity_scalar_4", "6x Optic"),
("gamepad_ads_advanced_sensitivity_scalar_5", "8x Optic"),
("gamepad_ads_advanced_sensitivity_scalar_6", "10x Optic"),
("gamepad_ads_advanced_sensitivity_scalar_7", "Seer Passive"),
]),
])
GROUP_ORDER = list(ALC_STRUCTURE.keys())
ALC_MAP = {k: (label, group) for group, items in ALC_STRUCTURE.items() for k, label in items}
INTRA_GROUP_ORDER = {group: [k for k, _ in items] for group, items in ALC_STRUCTURE.items()}
# ---------- Ranges for visual bars (normalized to 0..1) ----------
def alc_range(key: str) -> tuple[float, float]:
if key == "gamepad_custom_deadzone_in": # 0–50%
return (0.0, 50.0)
if key == "gamepad_custom_deadzone_out": # 1–30%
return (1.0, 30.0)
if key.endswith(("_time", "_delay")): # 0–100%
return (0.0, 100.0)
if key == "gamepad_custom_curve": # 0–30
return (0.0, 30.0)
if key.endswith("_turn_yaw") or key.endswith("_turn_pitch"): # 0–250
return (0.0, 250.0)
if key.endswith("_yaw") or key.endswith("_pitch"): # 0–500
return (0.0, 500.0)
if key.startswith("gamepad_ads_advanced_sensitivity_scalar_"): # 0.2–10.0
return (0.2, 10.0)
return (0.0, 1.0)
def is_percent_key(key: str) -> bool:
return (
key in ("gamepad_custom_deadzone_in", "gamepad_custom_deadzone_out")
or key.endswith("_time")
or key.endswith("_delay")
)
# ---------- Helpers ----------
def ensure_dir() -> None:
APEX_DIR.mkdir(parents=True, exist_ok=True)
def profiles() -> list[str]:
ensure_dir()
return sorted([p.stem for p in APEX_DIR.glob("*.cfg") if p.name.lower() != "profile.cfg"])
def read_cfg(path: Path) -> dict:
"""Reads Apex .cfg format: lines like key "value" or key value. Ignores // comments."""
if not path.exists():
return {}
data, rx = {}, re.compile(r'^\s*([A-Za-z0-9_\.]+)\s+("?)(.*?)\2\s*$')
with path.open("r", encoding="utf-8", errors="ignore") as f:
for raw in f:
line = raw.strip()
if not line or line.startswith("//"):
continue
if "//" in line:
line = line.split("//", 1)[0].strip()
if not line:
continue
m = rx.match(line)
if m:
data[m.group(1)] = m.group(3).strip()
return data
def parse_percent_value(raw: str) -> float:
"""Return percent as 0..100 from raw value that might be ratio (0..1), percent string '5%', or plain number."""
s = str(raw).strip()
try:
if s.endswith("%"):
return float(s[:-1])
v = float(s)
return v * 100.0 if v <= 1.0 else v
except Exception:
return 0.0
def parse_numeric(key: str, raw: str | float) -> float:
"""Return numeric in native units used by alc_range."""
if is_percent_key(key):
return parse_percent_value(raw)
try:
return float(str(raw).rstrip("%"))
except Exception:
return 0.0
def display_value(key: str, raw: str | float) -> str:
"""Format value like the game UI."""
v = parse_numeric(key, raw)
if is_percent_key(key):
return f"{v:.0f}%" if abs(v - round(v)) < 0.05 else f"{v:.1f}%"
if key.startswith("gamepad_ads_advanced_sensitivity_scalar_"):
return f"{v:.1f}"
if key == "gamepad_custom_curve":
return str(int(v)) if abs(v - round(v)) < 0.05 else f"{v:.1f}"
return str(int(v)) if abs(v - round(v)) < 0.05 else f"{v:.1f}"
def value_to_01(key: str, raw: str | float) -> float:
"""Normalize to 0..1 using in-game ranges, with unit conversion for percent keys."""
v = parse_numeric(key, raw)
lo, hi = alc_range(key)
if hi <= lo:
return 0.0
v = min(max(v, lo), hi) # clamp
return (v - lo) / (hi - lo)
def grouped_in_order(cfg: dict) -> dict[str, list[tuple[str, str, float, float]]]:
"""Return group -> list of (label, display_str, frac_0_1, native_numeric)."""
out = {g: [] for g in GROUP_ORDER}
for g in GROUP_ORDER:
for k in INTRA_GROUP_ORDER[g]:
if k in cfg:
label = ALC_MAP[k][0]
disp_str = display_value(k, cfg[k])
frac = value_to_01(k, cfg[k])
disp_num = parse_numeric(k, cfg[k])
out[g].append((label, disp_str, frac, disp_num))
return out
def backup_active() -> Path | None:
if not ACTIVE_CFG.exists():
return None
ts = datetime.now().strftime("%Y%m%d_%H%M%S")
dst = ACTIVE_CFG.with_name(f"profile.backup_{ts}.cfg")
shutil.copy2(ACTIVE_CFG, dst)
return dst
def apply_profile(name: str) -> str:
src = APEX_DIR / f"{name}.cfg"
if not src.exists():
return f"Profile not found: {src}"
b = backup_active()
try:
shutil.copy2(src, ACTIVE_CFG)
return f"Loaded {name}.cfg → profile.cfg" + (f" (backup: {b.name})" if b else "")
except Exception as e:
return f"Error applying: {e}"
def open_folder(path: Path) -> None:
try:
if os.name == "nt":
os.startfile(str(path)) # type: ignore
elif sys.platform == "darwin":
subprocess.run(["open", str(path)], check=False)
else:
subprocess.run(["xdg-open", str(path)], check=False)
except Exception:
pass
def new_profile_name(existing: list[str]) -> str:
now = datetime.now()
month_names = ['JANUARY','FEBRUARY','MARCH','APRIL','MAY','JUNE','JULY','AUGUST','SEPTEMBER','OCTOBER','NOVEMBER','DECEMBER']
base = f"{now.day:02d}{month_names[now.month-1]}{now.year}"
if base not in existing:
return base
i = 1
while f"{base}-{i}" in existing:
i += 1
return f"{base}-{i}"
def save_active_as_new() -> str:
ensure_dir()
if not ACTIVE_CFG.exists():
return "No active profile.cfg found to save."
name = new_profile_name(profiles())
try:
shutil.copy2(ACTIVE_CFG, APEX_DIR / f"{name}.cfg")
return f"Saved active profile as {name}.cfg"
except Exception as e:
return f"Error saving: {e}"
# ---------- UI + Animation ----------
class ALCViewer:
def __init__(self):
# UI ids
self.status = None
self.listbox = None
self.view_container = None
self.export_btn_id = None
self.import_modal_tag = "import_modal"
self.import_text_tag = "import_text"
self.import_save_btn = None
# layout
self.label_w = 260 # measured at runtime
# theme
self.bar_theme = None
# selection
self.sel = None
# animation state
# key -> (bar_id, text_id, start_frac, end_frac, start_num, end_num)
self.targets = {}
self.prev_frac = {}
self.prev_num = {}
self.animating = False
self.anim_start = 0.0
# export flash state
self.export_revert_lbl = None
self.export_deadline = 0.0
# ----------------- build -----------------
def build(self):
dpg.create_context()
dpg.create_viewport(title="Apex ALC Profiles", width=WINDOW_W, height=WINDOW_H)
# Progress bar theme: off-white fill
with dpg.theme() as bar_theme:
with dpg.theme_component(dpg.mvProgressBar):
dpg.add_theme_color(dpg.mvThemeCol_PlotHistogram, BAR_COLOR)
self.bar_theme = bar_theme
with dpg.window(label="Apex Legends ALC Profiles", tag="main_window",
width=WINDOW_W, height=WINDOW_H,
no_move=True, no_collapse=True):
with dpg.group(horizontal=True):
dpg.add_button(label="Refresh", callback=self.refresh)
dpg.add_button(label="Open Folder", callback=lambda: open_folder(APEX_DIR))
self.status = dpg.add_text("", wrap=900)
dpg.add_separator()
with dpg.group(horizontal=True):
# Sidebar
with dpg.child_window(width=320, height=-1, border=True):
dpg.add_text(f"Folder: {APEX_DIR}", bullet=True)
self.listbox = dpg.add_listbox(
items=profiles(),
num_items=28,
callback=self.on_profile_selected,
width=-1
)
dpg.add_text("Tip: Hover list & use mouse wheel or ↑/↓", color=(140, 140, 140))
# Right: tabs
with dpg.child_window(width=-1, height=-1, border=True):
with dpg.tab_bar():
with dpg.tab(label="View"):
dpg.add_text("Settings Preview", color=(180, 180, 180))
self.view_container = dpg.add_child_window(border=False, autosize_x=True, autosize_y=True)
with dpg.tab(label="Manage"):
dpg.add_text("Manage Profiles", color=(180, 180, 180))
dpg.add_spacer(height=6)
dpg.add_button(label="Load selected profile", width=260, callback=self.on_apply_clicked)
dpg.add_spacer(height=4)
dpg.add_button(label="Add current ALC settings as a new profile", width=460, callback=self.on_save_clicked)
dpg.add_spacer(height=8)
self.export_btn_id = dpg.add_button(label="Export selected profile", width=260, callback=self.on_export_clicked)
dpg.add_spacer(height=4)
dpg.add_button(label="Import profile", width=260, callback=self.on_import_clicked)
# Handlers (arrow keys + mouse wheel on list)
with dpg.handler_registry():
dpg.add_key_press_handler(dpg.mvKey_Up, callback=lambda: self._move_selection(-1))
dpg.add_key_press_handler(dpg.mvKey_Down, callback=lambda: self._move_selection(1))
dpg.add_mouse_wheel_handler(callback=self._on_wheel)
dpg.setup_dearpygui()
dpg.set_primary_window("main_window", True)
dpg.show_viewport()
self._measure_label_width()
# initial selection
items = dpg.get_item_configuration(self.listbox).get("items", [])
if items:
dpg.set_value(self.listbox, items[0])
self._load_profile(items[0])
else:
self._set_status("No profiles found. Put *.cfg files in the folder above.")
# Start per-frame heartbeat loop (schedule next frame each time)
self._schedule_next_frame()
dpg.start_dearpygui()
dpg.destroy_context()
# ----------------- heartbeat (per frame) -----------------
def _schedule_next_frame(self):
# IMPORTANT: in v2.1.0 this expects an absolute frame number
dpg.set_frame_callback(dpg.get_frame_count() + 1, self._heartbeat)
def _heartbeat(self):
now = time.perf_counter()
# Animation tick
if self.animating:
t = 1.0 if ANIM_DURATION <= 0 else min(1.0, (now - self.anim_start) / ANIM_DURATION)
# ease-out cubic
t_eased = 1 - (1 - t) ** 3
done = (t >= 1.0)
for key, (bar_id, text_id, s_frac, e_frac, s_num, e_num) in self.targets.items():
cur_frac = s_frac + (e_frac - s_frac) * t_eased
cur_num = s_num + (e_num - s_num) * t_eased
try:
dpg.set_value(bar_id, max(0.0, min(1.0, cur_frac)))
except Exception:
pass
try:
dpg.set_value(text_id, self._format_from_num(key, cur_num))
except Exception:
pass
if done:
self.animating = False
# Export label revert
if self.export_revert_lbl and now >= self.export_deadline:
try:
dpg.configure_item(self.export_btn_id, label=self.export_revert_lbl)
except Exception:
pass
self.export_revert_lbl = None
self.export_deadline = 0.0
# If import modal is open, live-enable/disable Save depending on content
if dpg.does_item_exist(self.import_modal_tag) and self.import_save_btn is not None:
try:
txt = dpg.get_value(self.import_text_tag) or ""
dpg.configure_item(self.import_save_btn, enabled=bool(txt.strip()))
except Exception:
pass
# Reschedule next frame
self._schedule_next_frame()
# ----------------- utilities -----------------
def _measure_label_width(self):
labels = [ALC_MAP[k][0] + ":" for g in GROUP_ORDER for k in INTRA_GROUP_ORDER[g]]
max_px = 0
for t in labels:
try:
w, _ = dpg.get_text_size(t)
except Exception:
w = len(t) * 8
if w > max_px:
max_px = w
self.label_w = int(max_px + LABEL_PADDING_PX)
def _set_status(self, text):
dpg.configure_item(self.status, default_value=text)
def _list_items(self) -> list[str]:
return dpg.get_item_configuration(self.listbox).get("items", []) or []
def _select_by_index(self, idx: int):
items = self._list_items()
if not items:
return
idx = max(0, min(len(items) - 1, idx))
dpg.set_value(self.listbox, items[idx])
self._load_profile(items[idx])
def _move_selection(self, delta: int):
items = self._list_items()
if not items:
return
cur = dpg.get_value(self.listbox)
try:
i = items.index(cur)
except Exception:
i = 0
self._select_by_index(i + delta)
def _on_wheel(self, sender, app_data):
# Only scroll-select when hovering the list
try:
if dpg.is_item_hovered(self.listbox):
# app_data is scroll delta: positive when scrolling up
step = -1 if app_data > 0 else (1 if app_data < 0 else 0)
if step:
self._move_selection(step)
except Exception:
pass
# ----------------- Manage actions -----------------
def refresh(self):
items = profiles()
dpg.configure_item(self.listbox, items=items)
if items:
cur = dpg.get_value(self.listbox)
self._load_profile(cur if cur in items else items[0])
if cur not in items:
dpg.set_value(self.listbox, items[0])
else:
self._clear_view()
self.sel = None
self._set_status("No profiles found. Put *.cfg files in the folder above.")
def on_profile_selected(self, _s, value):
if value:
self._load_profile(value)
def on_apply_clicked(self):
if not self.sel:
self._set_status("Pick a profile to load.")
return
self._set_status(apply_profile(self.sel))
def on_save_clicked(self):
msg = save_active_as_new()
self._set_status(msg)
if msg.startswith("Saved active profile as"):
self.refresh()
# ----------------- Export / Import -----------------
def on_export_clicked(self):
sel = dpg.get_value(self.listbox)
if not sel:
self._set_status("Pick a profile to export.")
return
path = APEX_DIR / f"{sel}.cfg"
try:
text = path.read_text(encoding="utf-8", errors="ignore")
except Exception as e:
self._set_status(f"Error reading profile: {e}")
return
# copy to clipboard
try:
dpg.set_clipboard_text(text)
except Exception:
pass
# flash label "Copied profile!"
try:
if self.export_btn_id is not None:
self.export_revert_lbl = dpg.get_item_label(self.export_btn_id)
dpg.configure_item(self.export_btn_id, label="Copied profile!")
self.export_deadline = time.perf_counter() + EXPORT_FLASH_SECS
except Exception:
pass
def on_import_clicked(self):
if dpg.does_item_exist(self.import_modal_tag):
dpg.delete_item(self.import_modal_tag)
with dpg.window(label="Import Profile", modal=True, no_resize=False, no_move=True,
width=720, height=520, tag=self.import_modal_tag):
# Buttons at top
with dpg.group(horizontal=True):
self.import_save_btn = dpg.add_button(label="Save", width=120, enabled=False, callback=self._do_import_profile)
dpg.add_button(label="Cancel", width=120, callback=lambda: dpg.delete_item(self.import_modal_tag))
dpg.add_spacer(height=6)
dpg.add_text("Paste profile text below (key value lines)")
dpg.add_input_text(tag=self.import_text_tag, multiline=True, width=-1, height=-1)
def _do_import_profile(self):
try:
text = dpg.get_value(self.import_text_tag) or ""
except Exception:
text = ""
text = text.strip()
if not text:
self._set_status("Nothing pasted to import.")
return
ts = datetime.now().strftime("%Y%m%d_%H%M%S")
name = f"imported_{ts}"
dest = APEX_DIR / f"{name}.cfg"
try:
# Ensure trailing newline and UTF-8
dest.write_text(text if text.endswith("\n") else text + "\n", encoding="utf-8")
self._set_status(f"Imported as {name}.cfg")
self.refresh()
if name in self._list_items():
dpg.set_value(self.listbox, name)
self._load_profile(name)
except Exception as e:
self._set_status(f"Error importing: {e}")
finally:
if dpg.does_item_exist(self.import_modal_tag):
dpg.delete_item(self.import_modal_tag)
# ----------------- View & animation -----------------
def _clear_view(self):
if not self.view_container:
return
for ch in (dpg.get_item_children(self.view_container, 1) or []):
dpg.delete_item(ch)
def _format_from_num(self, key: str, num: float) -> str:
if is_percent_key(key):
return f"{num:.0f}%" if abs(num - round(num)) < 0.05 else f"{num:.1f}%"
if key.startswith("gamepad_ads_advanced_sensitivity_scalar_"):
return f"{num:.1f}"
if key == "gamepad_custom_curve":
return str(int(num)) if abs(num - round(num)) < 0.05 else f"{num:.1f}"
return str(int(num)) if abs(num - round(num)) < 0.05 else f"{num:.1f}"
def _load_profile(self, name: str):
self.sel = name
cfg = read_cfg(APEX_DIR / f"{name}.cfg")
grouped = grouped_in_order(cfg)
# Build UI fresh each time (keeps layout simple)
self._clear_view()
self.targets.clear()
# Global table: Label | Value | Bar
with dpg.table(parent=self.view_container, header_row=False,
policy=dpg.mvTable_SizingStretchProp,
borders_innerV=True, borders_outerV=True,
borders_innerH=True, borders_outerH=True,
resizable=False):
dpg.add_table_column(label="Setting", init_width_or_weight=self.label_w, width_fixed=True)
dpg.add_table_column(label="Value", init_width_or_weight=VALUE_COL_WIDTH, width_fixed=True)
dpg.add_table_column(label="Bar", init_width_or_weight=1.0)
for g in GROUP_ORDER:
rows = grouped.get(g, [])
if not rows:
continue
# group header
with dpg.table_row():
dpg.add_text(g, color=(200, 200, 255)); dpg.add_text(""); dpg.add_text("")
# settings
for k in INTRA_GROUP_ORDER[g]:
# find row by label
match = next((r for r in rows if r[0] == ALC_MAP[k][0]), None)
if not match:
continue
label, disp_str, target_frac, target_num = match
# compute start from previous if present, else target (no pop on first load)
start_frac = self.prev_frac.get(k, target_frac)
start_num = self.prev_num.get(k, target_num)
with dpg.table_row():
dpg.add_text(f"{label}:", color=(255, 255, 255))
value_id = dpg.add_text(self._format_from_num(k, start_num), color=(200, 200, 200))
bar_id = dpg.add_progress_bar(default_value=start_frac, overlay="", width=-1)
dpg.bind_item_theme(bar_id, self.bar_theme)
# record animation targets
self.targets[k] = (bar_id, value_id, start_frac, target_frac, start_num, target_num)
# spacer row
with dpg.table_row():
dpg.add_text(""); dpg.add_text(""); dpg.add_text("")
# Store targets as prev for next switch
self.prev_frac = {k: t[3] for k, t in self.targets.items()} # end_frac
self.prev_num = {k: t[5] for k, t in self.targets.items()} # end_num
# Start animation
self.anim_start = time.perf_counter()
self.animating = True
self._set_status(f"Selected: {name}.cfg")
# ---------- Run ----------
if __name__ == "__main__":
ensure_dir()
ALCViewer().build()
```
Contributor guide
Assessment
This issue has not been assessed yet.