본문 바로가기
Programming/Processing

[ ProceJava ] 한글 입력 가능 텍스트필드 앱 만들기 - 2

by The Programmer 2025. 8. 19.

1. Introduction

 

프로세싱과 자바 Swing GUI 통합 - 002. 완전한 방식으로 한글 입출력이 가능한 텍스트필드 앱 만들기, 한영타자연습 프로그램 제작용 핵심 아이디어를 구현합니다.

 

이 예제는 프로세싱 고급 예제로 초보자용이 아닙니다. 제가 필요해서 올려둡니다. 이 예제는 Processing + Java 하이브리드(?) 앱 예제입니다. Processing 언어와 Java, Java GUI를 구현하는 Swing 등에 대한 충분한 사전 지식이 필요합니다.

 

이 프로그램은 프로세싱에서 자바 확장 기능을 이용하여 완전하게 한글 입력이 가능한 텍스트필드를 만드는 방법을 보여줍니다. 자바 코드가 대량 함유된(?) 사실상의 Java 앱이더라도 근본적으로는 Processing 프로그램입니다. 프로세싱 명령어들의 추가 확장이 100% 자유롭게 가능합니다.

 

프로세싱과 자바의 통합은 무난하나, ControlP5 자체 텍스트필드 객체와는 아직 완전하지 못합니다. ControlP5와의 연동은 잘 되지만, 한글 텍스트 직접 타이핑 입력 처리는 여전히 문제로 남아 있습니다. (간접 입력 방식은 해결됨.) 이 문제는 ControlP5 자체를 수정해야 할 듯하며, 이것은 이 앱의 제작 목표 범위를 벗어나는 일입니다. ControlP5의 내부 구조에 대해 아직 자세히 알지 못하기 때문에 그럴만한 역량이 되지 못합니다.

 

 

2. Code

 

// Update 1 코드임.

 

/*

Program Name : 프로세싱과 자바 통합 GUI - 002
Version : 1.0.2 Update 1
File Name : Java_TextField_Ex_009_.pde
Date : Aug. 19, 2025.
Licence : MIT, Not for Commercial Use Only.
Copyright : All rights are reserved. (C) James. 2025.

Notes : 
 
 * Processing + Java Swing JTextField Overlay + ControlP5 (v1.0.2 Update 1.)
 * 기본적인 한영 텍스트 입출력 테스트 완료. 
 * 한글 표시 가능 텍스트필드 테스트 앱 
 * 3방향 실시간 동기화
 * Swing IME 지원, CP5 UI와 연동.. ^.^;
 * ControlP5 연동은 잘 되지만, 한글 텍스트 직접 입력 처리는 문제로 남음.
 * CP5 자체를 수정해야. 이 앱의 제작 목표 범위를 넘어서는 일.
 * 같은 Processing 창 안에 Swing 필드 오버레이
 * 한글 IME 캐럿 항상 오른쪽 유지 오케이.

*/


import processing.awt.PSurfaceAWT;
import javax.swing.*;
import javax.swing.event.*;
import java.awt.*;
import java.awt.event.*;
import controlP5.*;
import processing.core.PFont;

PFont      pfont;

ControlP5 cp5;

JTextField swingField;
JButton submitBtn;
JLabel hint;
JFrame frame;
PSurfaceAWT.SmoothCanvas canvas;

String sharedText = "";

void settings() {
  size(720, 420);
    // JAVA2D 권장
}

void setup() {
  // Processing용 한글 PFont 로드
  try {
    pfont = createFont("D2Coding_.ttf", 16, true);
  } catch (Exception e) {
    println("PFont 로드 실패: " + e.getMessage());
    pfont = createFont("SansSerif", 16, true);
  }
  textFont(pfont);
  textSize(16);

  surface.setTitle("Processing + Java + ControlP5 (v1.0.2 Update 1.)");
  surface.setResizable(true);

  // ControlP5 구성
  cp5 = new ControlP5(this);
  cp5.addTextfield("cp5Field")
     .setLabel("CP5 입력창")
     .setPosition(20, 120)
     .setSize(240, 28)
     .setAutoClear(false)
     .setFont(pfont);   
// for KR font.;

  cp5.addButton("cp5Button")
     .setLabel("Swing 필드에 값 쓰기")
     .setPosition(270, 120)
     .setSize(180, 28)
     .setFont(pfont);   
// for KR font.
     // pfont = createFont("D2Coding_.ttf", 16, true);

  // Swing 오버레이 생성
  SwingUtilities.invokeLater(() -> {
    PSurfaceAWT awtSurface = (PSurfaceAWT) surface;
    canvas = (PSurfaceAWT.SmoothCanvas) awtSurface.getNative();
    frame = (JFrame) SwingUtilities.getWindowAncestor(canvas);

    swingField = new JTextField();
    swingField.setColumns(18);
    swingField.setFont(new Font("Malgun Gothic", Font.PLAIN, 16));

    submitBtn = new JButton("Submit(제출)");
    submitBtn.setFont(new Font("Malgun Gothic", Font.PLAIN, 14));

    hint = new JLabel("Swing <--> ControlP5 <--> 변수 실시간 동기화 (캐럿 유지)");
    hint.setFont(new Font("Malgun Gothic", Font.PLAIN, 12));
    hint.setForeground(new Color(0, 0, 0, 170));

    // Swing --> CP5 + 캐럿 이동
    DocumentListener swingListener = new DocumentListener() {
      public void insertUpdate(DocumentEvent e) { updateFromSwing(); moveCaretToEnd(); }
      public void removeUpdate(DocumentEvent e) { updateFromSwing(); moveCaretToEnd(); }
      public void changedUpdate(DocumentEvent e) { updateFromSwing(); moveCaretToEnd(); }
    };
    swingField.getDocument().addDocumentListener(swingListener);

 // (선택) IME 조합 중 캐럿 이동
    swingField.addInputMethodListener(new InputMethodListener() {
      public void inputMethodTextChanged(java.awt.event.InputMethodEvent e) { moveCaretToEnd(); }
      public void caretPositionChanged(java.awt.event.InputMethodEvent e) { moveCaretToEnd(); }
    });

    submitBtn.addActionListener(e -> {
      sharedText = swingField.getText();
      updateCP5Field();
      moveCaretToEnd();
    });

    // LayeredPane에 추가
    JLayeredPane lp = frame.getLayeredPane();
    lp.add(swingField, JLayeredPane.PALETTE_LAYER);
    lp.add(submitBtn, JLayeredPane.PALETTE_LAYER);
    lp.add(hint, JLayeredPane.PALETTE_LAYER);

    layoutOverlay();

    // 창 이동/리사이즈 대응
    frame.addComponentListener(new ComponentAdapter() {
      public void componentResized(ComponentEvent e) { layoutOverlay(); }
      public void componentMoved(ComponentEvent e) { layoutOverlay(); }
    });
  });
}

void draw() {
  background(245);

  fill(20);
  textSize(16);
  text("Processing Draw 영역", 24, 20);

  fill(0, 120, 180);
  textSize(14);
  text("공유 변수(sharedText): " + sharedText, 26, 40);
}

// ===== 동기화 로직 =====
void updateFromSwing() {
  sharedText = swingField.getText();
  updateCP5Field();
}

void updateFromCP5(String newVal) {
  sharedText = newVal;
  SwingUtilities.invokeLater(() -> {
    if (!swingField.getText().equals(newVal)) {
      swingField.setText(newVal);
      moveCaretToEnd();
    }
  });
}

void updateCP5Field() {
  if (!cp5.get(Textfield.class, "cp5Field").getText().equals(sharedText)) {
    cp5.get(Textfield.class, "cp5Field").setText(sharedText);
  }
}

// CP5 이벤트
public void cp5Field(String val) {
  updateFromCP5(val);
}
public void cp5Button() {
  updateFromCP5("CP5에서 보낸 값");
}

// ===== 캐럿 항상 끝으로 =====
void moveCaretToEnd() {
  SwingUtilities.invokeLater(() -> {
    if (swingField != null) {
      int len = swingField.getText().length();
      swingField.setCaretPosition(len);
    }
  });
}

// ===== Swing 오버레이 위치 지정 =====
void layoutOverlay() {
  Insets ins = frame.getInsets();
  int x0 = ins.left + 20;
  int y0 = ins.top + 20;
  int tfW = 240;
  int tfH = 28;

  swingField.setBounds(x0, y0, tfW, tfH);
  submitBtn.setBounds(x0 + tfW + 8, y0, 150, tfH);
  hint.setBounds(x0, y0 + tfH + 6, 400, 20);
}

// 종료시 정리
void dispose() {
  SwingUtilities.invokeLater(() -> {
    if (frame != null) {
      JLayeredPane lp = frame.getLayeredPane();
      lp.remove(swingField);
      lp.remove(submitBtn);
      lp.remove(hint);
      lp.revalidate();
      lp.repaint();
    }
  });
}

 

 

3. Result

 

[ ProceJava_TextField_v1_0_2_UP1_ ]

 

 

4. Notes

 

너무 길어질 것 같아서 간결하게 핵심만 정리합니다. 기억력의 한계로 노트 대신 보려는 용도입니다.

 

요약

1) 하나의 Processing 창 안에 Swing 컴포넌트가 ControlP5 UI와 함께 표시
2) 창 크기 변경·이동 시 오버레이 위치 자동 조정
3) 한글 입력 시 캐럿이 항상 오른쪽 끝에 고정 (IME 조합 중간 이동까지 반영)
4) 3방향 동기화: Swing <--> CP5 <--> 내부 변수

 

 

■ 코드의 전체 목적
A. Processing 스케치 내부에서 
1) Swing의 JTextField,
2) ControlP5의 Textfield 두 입력 필드를 동시에 사용하고,

B. 3방향 동기화 : 
1) Swing JTextField <--> ControlP5 Textfield
2) 두 필드 <--> 내부 공유 변수(sharedText)

C. 한글 IME 포함 모든 입력에서 캐럿(커서)을 항상 오른쪽 끝에 유지

 

D. 모든 UI를 같은 Processing 창 안에 겹쳐서 표시

 

 

주요 구성 요소
A. Swing 컴포넌트 (오버레이)
1) JTextField swingField : 메인 Swing 입력창, 한글 IME 안정 지원
2) JButton submitBtn : Swing에서 입력된 내용을 확정해 CP5 쪽에 반영하는 버튼
3) JLabel hint : 도움말/상태 표시용 라벨
4) JLayeredPane을 이용해 Processing 캔버스 위에 직접 배치

B. ControlP5 컴포넌트
1) cp5Field : 스케치 안쪽에 위치하는 ControlP5의 텍스트필드
2) cp5Button : Swing 입력창 값을 변경하기 위해 ControlP5 쪽에서 누르는 버튼

C. 내부 공유 변수
1) sharedText : 두 필드가 동시에 참조·갱신하는 문자열
--> Swing과 CP5가 항상 같은 값을 보도록 하는 데이터 허브 역할

 

 

동기화 로직
A. Swing --> 공유 변수 --> CP5
1) DocumentListener에서 Swing 필드 변경을 감지
2) updateFromSwing() 호출 --> sharedText 갱신 --> updateCP5Field()로 CP5 필드 업데이트
3) 캐럿 위치를 끝으로 이동 (moveCaretToEnd())

B. CP5 --> 공유 변수 --> Swing
1) ControlP5 cp5Field(String val) 이벤트에서 updateFromCP5(val) 호출
2) Swing 필드 텍스트 갱신 후 캐럿을 끝으로 이동

C. 버튼 액션
1) Swing submitBtn 클릭 : Swing 필드 내용 --> 공유 변수 --> CP5 반영
2) CP5 버튼 클릭 : 미리 지정한 값 --> 공유 변수 --> Swing 필드 반영

 

 

캐럿(커서) 항상 오른쪽 끝 유지
A. 구현 방법
1) DocumentListener 이벤트(insertUpdate, removeUpdate, changedUpdate)마다 moveCaretToEnd() 호출
2) CP5 --> Swing 갱신 시 moveCaretToEnd() 호출
3) InputMethodListener 추가
--> IME 조합 중간에도 캐럿 이동 시도 (필요 시 주석 가능)

B. 함수
1) 주요 코드

void moveCaretToEnd() {
  SwingUtilities.invokeLater(() -> {
    if (swingField != null) {
      int len = swingField.getText().length();
      swingField.setCaretPosition(len);
    }
  });
}

2) invokeLater를 쓰는 이유 : 

- 입력 이벤트 처리가 끝난 다음 시점에 캐럿을 이동시켜, IME 조합 끊김을 방지

 

 

Swing 오버레이 배치

1) Processing의 native AWT Canvas를 가져와 해당 캔버스의 부모 JFrame과 JLayeredPane를 참조
2) setBounds()로 원하는 픽셀 좌표에 직접 배치
3) frame.getInsets()로 타이틀바/테두리 영역 보정
4) ComponentListener로 창 이동·리사이즈 시 자동 재배치

 

 

동작 흐름 요약

A. Processing 실행 --> setup()
1) ControlP5 UI 생성
2) Swing UI를 같은 창의 LayeredPane에 추가
B. 사용자 Swing 필드 입력
1) 입력 감지(DocumentListener) --> 공유변수·CP5 업데이트 --> 캐럿 끝 이동
C. 사용자 CP5 필드 입력
1) 이벤트 함수(cp5Field)에서 Swing 필드 갱신 --> 캐럿 끝 이동
D. Swing 제출 버튼 / CP5 버튼 클릭
1) 공유변수 값을 반대쪽에 전달 --> 동기화 완성

 

 

기타
1) IME 완전 대응 : Swing 필드 덕에 한글/중문/일문 등 조합형 문자 입력 안정
2) 레이어드 UI : 별도 창 없이 하나의 Processing 프레임에서 Swing+CP5 혼합 가능
3) 명확한 상태 공유 : sharedText 하나로 두 UI가 항상 일치
4) 위치 동기화 : 창 이동·크기 변경에도 UI가 깨지지 않음

 

 

기타 응용 연습 과제

1) 타자 연습 프로그램 ...

2) 외부 파일 로딩 ...

 

 

5. Files

 

Java_TextField_Ex_009_UP1_.zip
16.41MB

 

 

6. Ref.

 

1) 황기태, [ 명품 Java Programming, 5E. ] 생능. 2024.

2) Casey Reas, Ben Fry 공저, [ Processing : A Programming Handbook, 2E. ] MIT. 2014.

3) 비슷한 주제, 이전 참고용 코드 : https://grammar.tistory.com/71

 

 

Happy Programming!

^.^;